diff --git a/packages/amino/src/signdoc.ts b/packages/amino/src/signdoc.ts index 3a6f56c1..ba319b4a 100644 --- a/packages/amino/src/signdoc.ts +++ b/packages/amino/src/signdoc.ts @@ -30,6 +30,7 @@ export interface StdSignDoc { readonly fee: StdFee; readonly msgs: readonly AminoMsg[]; readonly memo: string; + readonly timeout_height?: string; } function sortedObject(obj: any): any { @@ -61,6 +62,7 @@ export function makeSignDoc( memo: string | undefined, accountNumber: number | string, sequence: number | string, + timeout_height?: string, ): StdSignDoc { return { chain_id: chainId, @@ -69,6 +71,7 @@ export function makeSignDoc( fee: fee, msgs: msgs, memo: memo || "", + ...(timeout_height && { timeout_height }), }; } diff --git a/packages/cosmwasm-stargate/src/signingcosmwasmclient.spec.ts b/packages/cosmwasm-stargate/src/signingcosmwasmclient.spec.ts index 156dfe80..131be57d 100644 --- a/packages/cosmwasm-stargate/src/signingcosmwasmclient.spec.ts +++ b/packages/cosmwasm-stargate/src/signingcosmwasmclient.spec.ts @@ -1195,6 +1195,44 @@ describe("SigningCosmWasmClient", () => { client.disconnect(); }); + + it("works with a custom timeout height", async () => { + pendingWithoutWasmd(); + const wallet = await DirectSecp256k1HdWallet.fromMnemonic(alice.mnemonic, { prefix: wasmd.prefix }); + const client = await SigningCosmWasmClient.connectWithSigner(wasmd.endpoint, wallet, { + ...defaultSigningClientOptions, + }); + + const msg = MsgSend.fromPartial({ + fromAddress: alice.address0, + toAddress: alice.address0, + amount: [coin(1, "ucosm")], + }); + const msgAny: MsgSendEncodeObject = { + typeUrl: "/cosmos.bank.v1beta1.MsgSend", + value: msg, + }; + const fee = { + amount: coins(2000, "ucosm"), + gas: "222000", // 222k + }; + const memo = "Use your power wisely"; + const height = await client.getHeight(); + const signed = await client.sign( + alice.address0, + [msgAny], + fee, + memo, + undefined, + Long.fromNumber(height + 1), + ); + + // ensure signature is valid + const result = await client.broadcastTx(Uint8Array.from(TxRaw.encode(signed).finish())); + assertIsDeliverTxSuccess(result); + + client.disconnect(); + }); }); describe("legacy Amino mode", () => { @@ -1413,6 +1451,44 @@ describe("SigningCosmWasmClient", () => { client.disconnect(); }); + + it("works with custom timeoutHeight", async () => { + pendingWithoutWasmd(); + const wallet = await Secp256k1HdWallet.fromMnemonic(alice.mnemonic, { prefix: wasmd.prefix }); + const client = await SigningCosmWasmClient.connectWithSigner(wasmd.endpoint, wallet, { + ...defaultSigningClientOptions, + }); + + const msg = MsgSend.fromPartial({ + fromAddress: alice.address0, + toAddress: alice.address0, + amount: [coin(1, "ucosm")], + }); + const msgAny: MsgSendEncodeObject = { + typeUrl: "/cosmos.bank.v1beta1.MsgSend", + value: msg, + }; + const fee = { + amount: coins(2000, "ucosm"), + gas: "200000", + }; + const memo = "Use your tokens wisely"; + const height = await client.getHeight(); + const signed = await client.sign( + alice.address0, + [msgAny], + fee, + memo, + undefined, + Long.fromNumber(height + 1), + ); + + // ensure signature is valid + const result = await client.broadcastTx(Uint8Array.from(TxRaw.encode(signed).finish())); + assertIsDeliverTxSuccess(result); + + client.disconnect(); + }); }); }); }); diff --git a/packages/cosmwasm-stargate/src/signingcosmwasmclient.ts b/packages/cosmwasm-stargate/src/signingcosmwasmclient.ts index c1670d5e..e277ed04 100644 --- a/packages/cosmwasm-stargate/src/signingcosmwasmclient.ts +++ b/packages/cosmwasm-stargate/src/signingcosmwasmclient.ts @@ -644,6 +644,7 @@ export class SigningCosmWasmClient extends CosmWasmClient { fee: StdFee, memo: string, explicitSignerData?: SignerData, + timeoutHeight?: Long, ): Promise { let signerData: SignerData; if (explicitSignerData) { @@ -659,8 +660,8 @@ export class SigningCosmWasmClient extends CosmWasmClient { } return isOfflineDirectSigner(this.signer) - ? this.signDirect(signerAddress, messages, fee, memo, signerData) - : this.signAmino(signerAddress, messages, fee, memo, signerData); + ? this.signDirect(signerAddress, messages, fee, memo, signerData, timeoutHeight) + : this.signAmino(signerAddress, messages, fee, memo, signerData, timeoutHeight); } private async signAmino( @@ -669,6 +670,7 @@ export class SigningCosmWasmClient extends CosmWasmClient { fee: StdFee, memo: string, { accountNumber, sequence, chainId }: SignerData, + timeoutHeight?: Long, ): Promise { assert(!isOfflineDirectSigner(this.signer)); const accountFromSigner = (await this.signer.getAccounts()).find( @@ -680,13 +682,22 @@ export class SigningCosmWasmClient extends CosmWasmClient { const pubkey = encodePubkey(encodeSecp256k1Pubkey(accountFromSigner.pubkey)); const signMode = SignMode.SIGN_MODE_LEGACY_AMINO_JSON; const msgs = messages.map((msg) => this.aminoTypes.toAmino(msg)); - const signDoc = makeSignDocAmino(msgs, fee, chainId, memo, accountNumber, sequence); + const signDoc = makeSignDocAmino( + msgs, + fee, + chainId, + memo, + accountNumber, + sequence, + timeoutHeight?.toString(), + ); const { signature, signed } = await this.signer.signAmino(signerAddress, signDoc); const signedTxBody: TxBodyEncodeObject = { typeUrl: "/cosmos.tx.v1beta1.TxBody", value: { messages: signed.msgs.map((msg) => this.aminoTypes.fromAmino(msg)), memo: signed.memo, + timeoutHeight: timeoutHeight, }, }; const signedTxBodyBytes = this.registry.encode(signedTxBody); @@ -713,6 +724,7 @@ export class SigningCosmWasmClient extends CosmWasmClient { fee: StdFee, memo: string, { accountNumber, sequence, chainId }: SignerData, + timeoutHeight?: Long, ): Promise { assert(isOfflineDirectSigner(this.signer)); const accountFromSigner = (await this.signer.getAccounts()).find( @@ -727,6 +739,7 @@ export class SigningCosmWasmClient extends CosmWasmClient { value: { messages: messages, memo: memo, + timeoutHeight: timeoutHeight, }, }; const txBodyBytes = this.registry.encode(txBody); diff --git a/packages/stargate/src/signingstargateclient.spec.ts b/packages/stargate/src/signingstargateclient.spec.ts index 959aef7a..132ca76c 100644 --- a/packages/stargate/src/signingstargateclient.spec.ts +++ b/packages/stargate/src/signingstargateclient.spec.ts @@ -912,6 +912,44 @@ describe("SigningStargateClient", () => { const result = await client.broadcastTx(Uint8Array.from(TxRaw.encode(signed).finish())); assertIsDeliverTxSuccess(result); }); + + it("works with custom timeoutHeight", async () => { + pendingWithoutSimapp(); + const wallet = await DirectSecp256k1HdWallet.fromMnemonic(faucet.mnemonic); + const client = await SigningStargateClient.connectWithSigner( + simapp.tendermintUrl, + wallet, + defaultSigningClientOptions, + ); + + const msg = MsgSend.fromPartial({ + fromAddress: faucet.address0, + toAddress: faucet.address0, + amount: [coin(1, "ucosm")], + }); + const msgAny: MsgSendEncodeObject = { + typeUrl: "/cosmos.bank.v1beta1.MsgSend", + value: msg, + }; + const fee = { + amount: coins(2000, "ucosm"), + gas: "222000", // 222k + }; + const memo = "Use your power wisely"; + const height = await client.getHeight(); + const signed = await client.sign( + faucet.address0, + [msgAny], + fee, + memo, + undefined, + Long.fromNumber(height + 1), + ); + + // ensure signature is valid + const result = await client.broadcastTx(Uint8Array.from(TxRaw.encode(signed).finish())); + assertIsDeliverTxSuccess(result); + }); }); describe("legacy Amino mode", () => { @@ -1124,6 +1162,44 @@ describe("SigningStargateClient", () => { const result = await client.broadcastTx(Uint8Array.from(TxRaw.encode(signed).finish())); assertIsDeliverTxSuccess(result); }); + + it("works with custom timeoutHeight", async () => { + pendingWithoutSimapp(); + const wallet = await Secp256k1HdWallet.fromMnemonic(faucet.mnemonic); + const client = await SigningStargateClient.connectWithSigner( + simapp.tendermintUrl, + wallet, + defaultSigningClientOptions, + ); + + const msg = MsgSend.fromPartial({ + fromAddress: faucet.address0, + toAddress: faucet.address0, + amount: [coin(1, "ucosm")], + }); + const msgAny: MsgSendEncodeObject = { + typeUrl: "/cosmos.bank.v1beta1.MsgSend", + value: msg, + }; + const fee = { + amount: coins(2000, "ucosm"), + gas: "200000", + }; + const memo = "Use your tokens wisely"; + const height = await client.getHeight(); + const signed = await client.sign( + faucet.address0, + [msgAny], + fee, + memo, + undefined, + Long.fromNumber(height + 1), + ); + + // ensure signature is valid + const result = await client.broadcastTx(Uint8Array.from(TxRaw.encode(signed).finish())); + assertIsDeliverTxSuccess(result); + }); }); }); }); diff --git a/packages/stargate/src/signingstargateclient.ts b/packages/stargate/src/signingstargateclient.ts index a064d164..59650139 100644 --- a/packages/stargate/src/signingstargateclient.ts +++ b/packages/stargate/src/signingstargateclient.ts @@ -372,6 +372,7 @@ export class SigningStargateClient extends StargateClient { fee: StdFee, memo: string, explicitSignerData?: SignerData, + timeoutHeight?: Long, ): Promise { let signerData: SignerData; if (explicitSignerData) { @@ -387,8 +388,8 @@ export class SigningStargateClient extends StargateClient { } return isOfflineDirectSigner(this.signer) - ? this.signDirect(signerAddress, messages, fee, memo, signerData) - : this.signAmino(signerAddress, messages, fee, memo, signerData); + ? this.signDirect(signerAddress, messages, fee, memo, signerData, timeoutHeight) + : this.signAmino(signerAddress, messages, fee, memo, signerData, timeoutHeight); } private async signAmino( @@ -397,6 +398,7 @@ export class SigningStargateClient extends StargateClient { fee: StdFee, memo: string, { accountNumber, sequence, chainId }: SignerData, + timeoutHeight?: Long, ): Promise { assert(!isOfflineDirectSigner(this.signer)); const accountFromSigner = (await this.signer.getAccounts()).find( @@ -408,11 +410,20 @@ export class SigningStargateClient extends StargateClient { const pubkey = encodePubkey(encodeSecp256k1Pubkey(accountFromSigner.pubkey)); const signMode = SignMode.SIGN_MODE_LEGACY_AMINO_JSON; const msgs = messages.map((msg) => this.aminoTypes.toAmino(msg)); - const signDoc = makeSignDocAmino(msgs, fee, chainId, memo, accountNumber, sequence); + const signDoc = makeSignDocAmino( + msgs, + fee, + chainId, + memo, + accountNumber, + sequence, + timeoutHeight?.toString(), + ); const { signature, signed } = await this.signer.signAmino(signerAddress, signDoc); const signedTxBody = { messages: signed.msgs.map((msg) => this.aminoTypes.fromAmino(msg)), memo: signed.memo, + timeoutHeight: timeoutHeight, }; const signedTxBodyEncodeObject: TxBodyEncodeObject = { typeUrl: "/cosmos.tx.v1beta1.TxBody", @@ -442,6 +453,7 @@ export class SigningStargateClient extends StargateClient { fee: StdFee, memo: string, { accountNumber, sequence, chainId }: SignerData, + timeoutHeight?: Long, ): Promise { assert(isOfflineDirectSigner(this.signer)); const accountFromSigner = (await this.signer.getAccounts()).find( @@ -456,6 +468,7 @@ export class SigningStargateClient extends StargateClient { value: { messages: messages, memo: memo, + timeoutHeight: timeoutHeight, }, }; const txBodyBytes = this.registry.encode(txBodyEncodeObject);