From 2d6bbb079f4fec630726164b487b0db41bc494ec Mon Sep 17 00:00:00 2001 From: Simon Warta Date: Mon, 17 May 2021 12:12:37 +0200 Subject: [PATCH] Handle CheckTx errors properly --- .../cosmwasm-stargate/src/cosmwasmclient.ts | 22 ++++++-- packages/stargate/src/stargateclient.spec.ts | 52 +++++++++++++++++++ packages/stargate/src/stargateclient.ts | 31 +++++++++-- 3 files changed, 99 insertions(+), 6 deletions(-) diff --git a/packages/cosmwasm-stargate/src/cosmwasmclient.ts b/packages/cosmwasm-stargate/src/cosmwasmclient.ts index 33f71156..7299ab56 100644 --- a/packages/cosmwasm-stargate/src/cosmwasmclient.ts +++ b/packages/cosmwasm-stargate/src/cosmwasmclient.ts @@ -207,6 +207,17 @@ export class CosmWasmClient { if (this.tmClient) this.tmClient.disconnect(); } + /** + * Broadcasts a signed transaction to the network and monitors its inclusion in a block. + * + * If broadcasting is rejected by the node for some reason (e.g. because of a CheckTx failure), + * an error is thrown. + * + * If the transaction is not included in a block before the provided timeout, this errors with a `TimeoutError`. + * + * If the transaction is included in a block, a `BroadcastTxResponse` is returned. The caller then + * usually needs check for execution success or failure. + */ // NOTE: This method is tested against slow chains and timeouts in the @cosmjs/stargate package. // Make sure it is kept in sync! public async broadcastTx( @@ -240,10 +251,15 @@ export class CosmWasmClient { : pollForTx(txId); }; + const broadcasted = await this.forceGetTmClient().broadcastTxSync({ tx }); + if (broadcasted.code) { + throw new Error( + `Broadcasting transaction failed with code ${broadcasted.code} (codespace: ${broadcasted.codeSpace}). Log: ${broadcasted.log}`, + ); + } + const transactionId = toHex(broadcasted.hash).toUpperCase(); return new Promise((resolve, reject) => - this.forceGetTmClient() - .broadcastTxSync({ tx }) - .then(({ hash }) => pollForTx(toHex(hash).toUpperCase())) + pollForTx(transactionId) .then(resolve, reject) .finally(() => clearTimeout(txPollTimeout)), ); diff --git a/packages/stargate/src/stargateclient.spec.ts b/packages/stargate/src/stargateclient.spec.ts index 3130ab32..ca2d2b7c 100644 --- a/packages/stargate/src/stargateclient.spec.ts +++ b/packages/stargate/src/stargateclient.spec.ts @@ -337,6 +337,58 @@ describe("StargateClient", () => { client.disconnect(); }); + it("errors immediately for a CheckTx failure", async () => { + pendingWithoutSimapp(); + const client = await StargateClient.connect(simapp.tendermintUrl); + const wallet = await DirectSecp256k1HdWallet.fromMnemonic(faucet.mnemonic); + const [{ address, pubkey: pubkeyBytes }] = await wallet.getAccounts(); + const pubkey = encodePubkey({ + type: "tendermint/PubKeySecp256k1", + value: toBase64(pubkeyBytes), + }); + const registry = new Registry(); + const invalidRecipientAddress = "tgrade1z363ulwcrxged4z5jswyt5dn5v3lzsemwz9ewj"; // wrong bech32 prefix + const txBodyFields: TxBodyEncodeObject = { + typeUrl: "/cosmos.tx.v1beta1.TxBody", + value: { + messages: [ + { + typeUrl: "/cosmos.bank.v1beta1.MsgSend", + value: { + fromAddress: address, + toAddress: invalidRecipientAddress, + amount: [ + { + denom: "ucosm", + amount: "1234567", + }, + ], + }, + }, + ], + }, + }; + const txBodyBytes = registry.encode(txBodyFields); + const { accountNumber, sequence } = (await client.getSequence(address))!; + const feeAmount = coins(2000, "ucosm"); + const gasLimit = 200000; + const authInfoBytes = makeAuthInfoBytes([pubkey], feeAmount, gasLimit, sequence); + + const chainId = await client.getChainId(); + const signDoc = makeSignDoc(txBodyBytes, authInfoBytes, chainId, accountNumber); + const { signature } = await wallet.signDirect(address, signDoc); + const txRaw = TxRaw.fromPartial({ + bodyBytes: txBodyBytes, + authInfoBytes: authInfoBytes, + signatures: [fromBase64(signature.signature)], + }); + const txRawBytes = Uint8Array.from(TxRaw.encode(txRaw).finish()); + + await expectAsync(client.broadcastTx(txRawBytes)).toBeRejectedWithError(/invalid recipient address/i); + + client.disconnect(); + }); + it("respects user timeouts rather than RPC timeouts", async () => { pendingWithoutSlowSimapp(); const client = await StargateClient.connect(slowSimapp.tendermintUrl); diff --git a/packages/stargate/src/stargateclient.ts b/packages/stargate/src/stargateclient.ts index 1d198d38..fb7e2011 100644 --- a/packages/stargate/src/stargateclient.ts +++ b/packages/stargate/src/stargateclient.ts @@ -99,6 +99,15 @@ export interface BroadcastTxSuccess { readonly gasWanted: number; } +/** + * The response after sucessfully broadcasting a transaction. + * Success or failure refer to the execution result. + * + * The name is a bit misleading as this contains the result of the execution + * in a block. Both `BroadcastTxSuccess` and `BroadcastTxFailure` contain a height + * field, which is the height of the block that contains the transaction. This means + * transactions that were never included in a block cannot be expressed with this type. + */ export type BroadcastTxResponse = BroadcastTxSuccess | BroadcastTxFailure; export function isBroadcastTxFailure(result: BroadcastTxResponse): result is BroadcastTxFailure { @@ -292,6 +301,17 @@ export class StargateClient { if (this.tmClient) this.tmClient.disconnect(); } + /** + * Broadcasts a signed transaction to the network and monitors its inclusion in a block. + * + * If broadcasting is rejected by the node for some reason (e.g. because of a CheckTx failure), + * an error is thrown. + * + * If the transaction is not included in a block before the provided timeout, this errors with a `TimeoutError`. + * + * If the transaction is included in a block, a `BroadcastTxResponse` is returned. The caller then + * usually needs check for execution success or failure. + */ public async broadcastTx( tx: Uint8Array, timeoutMs = 60_000, @@ -323,10 +343,15 @@ export class StargateClient { : pollForTx(txId); }; + const broadcasted = await this.forceGetTmClient().broadcastTxSync({ tx }); + if (broadcasted.code) { + throw new Error( + `Broadcasting transaction failed with code ${broadcasted.code} (codespace: ${broadcasted.codeSpace}). Log: ${broadcasted.log}`, + ); + } + const transactionId = toHex(broadcasted.hash).toUpperCase(); return new Promise((resolve, reject) => - this.forceGetTmClient() - .broadcastTxSync({ tx }) - .then(({ hash }) => pollForTx(toHex(hash).toUpperCase())) + pollForTx(transactionId) .then(resolve, reject) .finally(() => clearTimeout(txPollTimeout)), );