diff --git a/CHANGELOG.md b/CHANGELOG.md index 7a2a128c..28109c25 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added ### Changed +* [\#116](https://github.com/Finschia/finschia-js/pull/116) Update swapAndBridge function ### Deprecated diff --git a/packages/finschia/src/finschiaclient.ts b/packages/finschia/src/finschiaclient.ts index dedb091f..81b4e764 100644 --- a/packages/finschia/src/finschiaclient.ts +++ b/packages/finschia/src/finschiaclient.ts @@ -580,4 +580,57 @@ export class FinschiaClient { }; }); } + + /** + * poll and get the tx result of txHash. + */ + public async pollForTxResult( + txId: Uint8Array, + timeoutMs = 60_000, + pollIntervalMs = 3_000, + ): Promise { + let timedOut = false; + const txPollTimeout = setTimeout(() => { + timedOut = true; + }, timeoutMs); + const pollForTx = async (txId: string): Promise => { + if (timedOut) { + throw new TimeoutError( + `Transaction with ID ${txId} was submitted but was not yet found on the chain. You might want to check later. There was a wait of ${ + timeoutMs / 10000 + } seconds.`, + txId, + ); + } + await sleep(pollIntervalMs); + const result = await this.getTx(txId); + return result === null + ? pollForTx(txId) + : { + code: result.code, + height: result.height, + txIndex: result.txIndex, + events: result.events, + rawLog: result.rawLog, + transactionHash: txId, + msgResponses: result.msgResponses, + gasUsed: result.gasUsed, + gasWanted: result.gasWanted, + }; + }; + + const transactionId = toHex(txId).toUpperCase(); + return new Promise((resolve, reject) => + pollForTx(transactionId).then( + (value) => { + clearTimeout(txPollTimeout); + resolve(value); + }, + (error) => { + clearTimeout(txPollTimeout); + reject(error); + }, + ), + ); + } } diff --git a/packages/finschia/src/signingfinschiaclient.ts b/packages/finschia/src/signingfinschiaclient.ts index 0953c2b7..902cd39e 100644 --- a/packages/finschia/src/signingfinschiaclient.ts +++ b/packages/finschia/src/signingfinschiaclient.ts @@ -8,16 +8,14 @@ import { InstantiateResult, JsonObject, MigrateResult, - MsgInstantiateContract2EncodeObject, - MsgStoreCodeEncodeObject, - UploadResult, -} from "@cosmjs/cosmwasm-stargate"; -import { MsgClearAdminEncodeObject, MsgExecuteContractEncodeObject, + MsgInstantiateContract2EncodeObject, MsgInstantiateContractEncodeObject, MsgMigrateContractEncodeObject, + MsgStoreCodeEncodeObject, MsgUpdateAdminEncodeObject, + UploadResult, } from "@cosmjs/cosmwasm-stargate"; import { sha256 } from "@cosmjs/crypto"; import { fromBase64, toHex, toUtf8 } from "@cosmjs/encoding"; @@ -62,11 +60,11 @@ import { MsgClearAdmin, MsgExecuteContract, MsgInstantiateContract, + MsgInstantiateContract2, MsgMigrateContract, + MsgStoreCode, MsgUpdateAdmin, } from "cosmjs-types/cosmwasm/wasm/v1/tx"; -import { MsgInstantiateContract2 } from "cosmjs-types/cosmwasm/wasm/v1/tx"; -import { MsgStoreCode } from "cosmjs-types/cosmwasm/wasm/v1/tx"; import { AccessConfig } from "cosmjs-types/cosmwasm/wasm/v1/types"; import { MsgTransfer } from "cosmjs-types/ibc/applications/transfer/v1/tx"; import { Height } from "cosmjs-types/ibc/core/client/v1/client"; @@ -100,6 +98,15 @@ export interface UploadAndInstantiateResult { readonly gasUsed: number; } +export interface SwapAndBridgeOptions { + readonly gas?: StdFee | number; + readonly memo?: string; + /** The balance to swap and bridge. */ + readonly amount?: string | number; + /** Set broadcast mode. If true, BROADCAST_ASYNC_MODE, if false, BROADCAST_SYNC_MODE */ + readonly asyncBroadcast?: boolean; +} + function createDeliverTxResponseErrorMessage(result: DeliverTxResponse): string { return `Error when broadcasting tx ${result.transactionHash} at height ${result.height}. Code: ${result.code}; Raw log: ${result.rawLog}`; } @@ -560,9 +567,8 @@ export class SigningFinschiaClient extends FinschiaClient { public async swapAndBridge( senderAddress: string, toAddress: string, - gas: StdFee | number = 150_000, - memo = "", - ): Promise { + { gas = 150_000, memo = "", amount = undefined, asyncBroadcast = false }: SwapAndBridgeOptions = {}, + ): Promise { // query swap rate const swapsRes = await this.forceGetQueryClientForV4().fswap.swaps(); if (swapsRes.swaps.length == 0) { @@ -587,13 +593,24 @@ export class SigningFinschiaClient extends FinschiaClient { amount: coins(gasLimit * 0.015, swap.fromDenom), gas: gasLimit.toString(), }; - } else { + } else if (gas !== undefined) { usedFee = gas; + } else { + throw new Error("No gas value provided."); + } + + let swapAmount: bigint; + if (amount === undefined) { + swapAmount = BigInt(balance.amount) - BigInt(usedFee.amount[0].amount); + } else { + if (BigInt(amount) + BigInt(usedFee.amount[0].amount) > BigInt(balance.amount)) { + throw new Error("no enough balance"); + } + swapAmount = BigInt(amount); } // calculate swapRate and total swapAmount, amount to transfer by bridge const swapRate = Decimal.fromAtomics(swap.swapRate, 18); - const swapAmount = BigInt(balance.amount) - BigInt(usedFee.amount[0].amount); const swappedAmount = swapAmount * BigInt(swapRate.toString()); const msgSwap: MsgSwapEncodeObject = { @@ -613,12 +630,16 @@ export class SigningFinschiaClient extends FinschiaClient { }), }; - const result = await this.signAndBroadcast(senderAddress, [msgSwap, msgBridge], usedFee, memo); - if (isDeliverTxFailure(result)) { - throw new Error(createDeliverTxResponseErrorMessage(result)); + const txRaw = await this.sign(senderAddress, [msgSwap, msgBridge], usedFee, memo); + const txBytes = TxRaw.encode(txRaw).finish(); + + // If BROADCAST_ASYNC_MODE + if (asyncBroadcast) { + const broadcasted = await this.forceGetTmClient().broadcastTxAsync({ tx: txBytes }); + return broadcasted.hash; } - return result; + return this.broadcastTx(txBytes, this.broadcastTimeoutMs, this.broadcastPollIntervalMs); } /** diff --git a/packages/finschia/src/signingfinschiaclientv4.spec.ts b/packages/finschia/src/signingfinschiaclientv4.spec.ts index a6728bc6..c9f1470b 100644 --- a/packages/finschia/src/signingfinschiaclientv4.spec.ts +++ b/packages/finschia/src/signingfinschiaclientv4.spec.ts @@ -1,6 +1,7 @@ import { coins, Secp256k1HdWallet } from "@cosmjs/amino"; import { DirectSecp256k1HdWallet } from "@cosmjs/proto-signing"; -import { assertIsDeliverTxSuccess } from "@cosmjs/stargate"; +import { assertIsDeliverTxSuccess, DeliverTxResponse } from "@cosmjs/stargate"; +import { assertDefined } from "@cosmjs/utils"; import { makeLinkPath } from "./paths"; import { SigningFinschiaClient } from "./signingfinschiaclient"; @@ -13,7 +14,7 @@ describe("SigningFinschiaClient", () => { }; describe("swapAndBridge", () => { - it("works", async () => { + it("works (Async broadcast)", async () => { pendingWithoutSimapp(); const wallet = await DirectSecp256k1HdWallet.fromMnemonic(faucet.mnemonic, { hdPaths: [makeLinkPath(0), makeLinkPath(200)], @@ -29,10 +30,53 @@ describe("SigningFinschiaClient", () => { const result = await client.sendTokens(addrs[0], addrs[1], coins(1000000, "cony"), defaultFee); assertIsDeliverTxSuccess(result); } + // swap & bridge partial balance const toAddr = "0xf7bAc63fc7CEaCf0589F25454Ecf5C2ce904997c"; - const result = await client.swapAndBridge(addrs[1], toAddr); - assertIsDeliverTxSuccess(result); + const result = await client.swapAndBridge(addrs[1], toAddr, { amount: 500_000 }); + assertIsDeliverTxSuccess(result as DeliverTxResponse); + + const balanceCony = await client.getBalance(addrs[1], "cony"); + expect(balanceCony.amount).toEqual("497750"); + const balance = await client.getBalance(addrs[1], "pdt"); + expect(balance.amount).toEqual("0"); + + // swap & bridge all balance + const result2 = await client.swapAndBridge(addrs[1], toAddr); + assertIsDeliverTxSuccess(result2 as DeliverTxResponse); + + const balanceCony2 = await client.getBalance(addrs[1], "cony"); + expect(balanceCony2.amount).toEqual("0"); + const balance2 = await client.getBalance(addrs[1], "pdt"); + expect(balance2.amount).toEqual("0"); + }); + + it("works (Sync broadcast)", async () => { + pendingWithoutSimapp(); + const wallet = await DirectSecp256k1HdWallet.fromMnemonic(faucet.mnemonic, { + hdPaths: [makeLinkPath(0), makeLinkPath(200)], + prefix: simapp.prefix, + }); + const options = { ...defaultSigningClientOptions, prefix: simapp.prefix }; + const client = await SigningFinschiaClient.connectWithSigner(simapp.tendermintUrl, wallet, options); + + const addrs = (await wallet.getAccounts()).map((item) => item.address); + + // bank send for test + { + const result = await client.sendTokens(addrs[0], addrs[1], coins(1000000, "cony"), defaultFee); + assertIsDeliverTxSuccess(result); + } + const toAddr = "0xf7bAc63fc7CEaCf0589F25454Ecf5C2ce904997c"; + const result = await client.swapAndBridge(addrs[1], toAddr, { asyncBroadcast: true }); + assertDefined(result); + + { + const txRes = await client.pollForTxResult(result as Uint8Array); + assertIsDeliverTxSuccess(txRes); + } + const balanceCony = await client.getBalance(addrs[1], "cony"); + expect(balanceCony.amount).toEqual("0"); const balance = await client.getBalance(addrs[1], "pdt"); expect(balance.amount).toEqual("0"); }); @@ -55,8 +99,10 @@ describe("SigningFinschiaClient", () => { } const toAddr = "0xf7bAc63fc7CEaCf0589F25454Ecf5C2ce904997c"; const result = await client.swapAndBridge(addrs[1], toAddr); - assertIsDeliverTxSuccess(result); + assertIsDeliverTxSuccess(result as DeliverTxResponse); + const balanceCony = await client.getBalance(addrs[1], "cony"); + expect(balanceCony.amount).toEqual("0"); const balance = await client.getBalance(addrs[1], "pdt"); expect(balance.amount).toEqual("0"); }); diff --git a/packages/finschia/src/utils.ts b/packages/finschia/src/utils.ts index 2be6dba1..db63a2cc 100644 --- a/packages/finschia/src/utils.ts +++ b/packages/finschia/src/utils.ts @@ -1,4 +1,3 @@ -import { _instantiate2AddressIntermediate } from "@cosmjs/cosmwasm-stargate"; import { Decimal, Uint64 } from "@cosmjs/math"; import { Duration } from "cosmjs-types/google/protobuf/duration"; import Long from "long";