Handle CheckTx errors properly

This commit is contained in:
Simon Warta 2021-05-17 12:12:37 +02:00
parent efdfd8a733
commit 2d6bbb079f
3 changed files with 99 additions and 6 deletions

View File

@ -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)),
);

View File

@ -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);

View File

@ -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)),
);