From 9bcc43977b50e24da0b076ce4c5dd97ed4d57952 Mon Sep 17 00:00:00 2001 From: DavideSegullo Date: Mon, 27 Mar 2023 11:55:49 +0200 Subject: [PATCH 01/12] feat: :sparkles: add sign and broadcast without polling --- .../cosmwasm-stargate/src/cosmwasmclient.ts | 27 +++++++++++++++ .../src/signingcosmwasmclient.ts | 34 +++++++++++++++++++ .../stargate/src/signingstargateclient.ts | 26 ++++++++++++++ packages/stargate/src/stargateclient.ts | 27 +++++++++++++++ 4 files changed, 114 insertions(+) diff --git a/packages/cosmwasm-stargate/src/cosmwasmclient.ts b/packages/cosmwasm-stargate/src/cosmwasmclient.ts index b52d54e987..6ae7bb43b6 100644 --- a/packages/cosmwasm-stargate/src/cosmwasmclient.ts +++ b/packages/cosmwasm-stargate/src/cosmwasmclient.ts @@ -332,6 +332,33 @@ export class CosmWasmClient { ); } + /** + * Broadcasts a signed transaction to the network without monitoring it. + * + * 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 broadcasted, a `string` containing the hash of the transaction is returned. The caller then + * usually needs to check if the transaction was included in a block and was successful. + * + * @returns Returns the hash of the transaction + */ + public async broadcastTxWithoutPolling( + tx: Uint8Array, + ): Promise { + const broadcasted = await this.forceGetTmClient().broadcastTxSync({ tx }); + + if (broadcasted.code) { + return Promise.reject( + new BroadcastTxError(broadcasted.code, broadcasted.codespace ?? "", broadcasted.log), + ); + } + + const transactionId = toHex(broadcasted.hash).toUpperCase(); + + return transactionId; + } + /** * getCodes() returns all codes and is just looping through all pagination pages. * diff --git a/packages/cosmwasm-stargate/src/signingcosmwasmclient.ts b/packages/cosmwasm-stargate/src/signingcosmwasmclient.ts index 03f6fe2fdc..8103fcd75a 100644 --- a/packages/cosmwasm-stargate/src/signingcosmwasmclient.ts +++ b/packages/cosmwasm-stargate/src/signingcosmwasmclient.ts @@ -557,6 +557,40 @@ export class SigningCosmWasmClient extends CosmWasmClient { return this.broadcastTx(txBytes, this.broadcastTimeoutMs, this.broadcastPollIntervalMs); } + /** + * Creates a transaction with the given messages, fee and memo. Then signs and broadcasts the transaction. + * + * This method is useful if you want to send a transaction in broadcast, + * without waiting for it to be placed inside a block, because for example + * I would like to receive the hash to later track the transaction with another tool. + * + * @param signerAddress The address that will sign transactions using this instance. The signer must be able to sign with this address. + * @param messages + * @param fee + * @param memo + * + * @returns Returns the hash of the transaction + */ + public async signAndBroadcastWithoutPolling( + signerAddress: string, + messages: readonly EncodeObject[], + fee: StdFee | "auto" | number, + memo = "", + ): Promise { + let usedFee: StdFee; + if (fee == "auto" || typeof fee === "number") { + assertDefined(this.gasPrice, "Gas price must be set in the client options when auto gas is used."); + const gasEstimation = await this.simulate(signerAddress, messages, memo); + const multiplier = typeof fee === "number" ? fee : 1.3; + usedFee = calculateFee(Math.round(gasEstimation * multiplier), this.gasPrice); + } else { + usedFee = fee; + } + const txRaw = await this.sign(signerAddress, messages, usedFee, memo); + const txBytes = TxRaw.encode(txRaw).finish(); + return this.broadcastTxWithoutPolling(txBytes); + } + public async sign( signerAddress: string, messages: readonly EncodeObject[], diff --git a/packages/stargate/src/signingstargateclient.ts b/packages/stargate/src/signingstargateclient.ts index d782e08c72..84ab551cd7 100644 --- a/packages/stargate/src/signingstargateclient.ts +++ b/packages/stargate/src/signingstargateclient.ts @@ -310,6 +310,32 @@ export class SigningStargateClient extends StargateClient { return this.broadcastTx(txBytes, this.broadcastTimeoutMs, this.broadcastPollIntervalMs); } + /** + * This method is useful if you want to send a transaction in broadcast, + * without waiting for it to be placed inside a block, because for example + * I would like to receive the hash to later track the transaction with another tool. + * @returns Returns the hash of the transaction + */ + public async signAndBroadcastWithoutPolling( + signerAddress: string, + messages: readonly EncodeObject[], + fee: StdFee | "auto" | number, + memo = "", + ): Promise { + let usedFee: StdFee; + if (fee == "auto" || typeof fee === "number") { + assertDefined(this.gasPrice, "Gas price must be set in the client options when auto gas is used."); + const gasEstimation = await this.simulate(signerAddress, messages, memo); + const multiplier = typeof fee === "number" ? fee : 1.3; + usedFee = calculateFee(Math.round(gasEstimation * multiplier), this.gasPrice); + } else { + usedFee = fee; + } + const txRaw = await this.sign(signerAddress, messages, usedFee, memo); + const txBytes = TxRaw.encode(txRaw).finish(); + return this.broadcastTxWithoutPolling(txBytes); + } + /** * Gets account number and sequence from the API, creates a sign doc, * creates a single signature and assembles the signed transaction. diff --git a/packages/stargate/src/stargateclient.ts b/packages/stargate/src/stargateclient.ts index d4969afa98..56883816d7 100644 --- a/packages/stargate/src/stargateclient.ts +++ b/packages/stargate/src/stargateclient.ts @@ -491,6 +491,33 @@ export class StargateClient { ); } + /** + * Broadcasts a signed transaction to the network without monitoring it. + * + * 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 broadcasted, a `string` containing the hash of the transaction is returned. The caller then + * usually needs to check if the transaction was included in a block and was successful. + * + * @returns Returns the hash of the transaction + */ + public async broadcastTxWithoutPolling( + tx: Uint8Array, + ): Promise { + const broadcasted = await this.forceGetTmClient().broadcastTxSync({ tx }); + + if (broadcasted.code) { + return Promise.reject( + new BroadcastTxError(broadcasted.code, broadcasted.codespace ?? "", broadcasted.log), + ); + } + + const transactionId = toHex(broadcasted.hash).toUpperCase(); + + return transactionId; + } + private async txsQuery(query: string): Promise { const results = await this.forceGetTmClient().txSearchAll({ query: query }); return results.txs.map((tx) => { From da5ab1856491113951dc86738151ae1a121602d5 Mon Sep 17 00:00:00 2001 From: DavideSegullo Date: Mon, 27 Mar 2023 12:44:31 +0200 Subject: [PATCH 02/12] style: :rotating_light: fix lint errors --- packages/stargate/src/stargateclient.ts | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/packages/stargate/src/stargateclient.ts b/packages/stargate/src/stargateclient.ts index 56883816d7..69997d1858 100644 --- a/packages/stargate/src/stargateclient.ts +++ b/packages/stargate/src/stargateclient.ts @@ -499,12 +499,10 @@ export class StargateClient { * * If the transaction is broadcasted, a `string` containing the hash of the transaction is returned. The caller then * usually needs to check if the transaction was included in a block and was successful. - * + * * @returns Returns the hash of the transaction */ - public async broadcastTxWithoutPolling( - tx: Uint8Array, - ): Promise { + public async broadcastTxWithoutPolling(tx: Uint8Array): Promise { const broadcasted = await this.forceGetTmClient().broadcastTxSync({ tx }); if (broadcasted.code) { From 53e39ab24b9bc0f18ac44bbdf13c8f2bc1d7f75e Mon Sep 17 00:00:00 2001 From: DavideSegullo Date: Mon, 27 Mar 2023 12:54:59 +0200 Subject: [PATCH 03/12] style: :rotating_light: fix lint errors --- packages/cosmwasm-stargate/src/cosmwasmclient.ts | 6 ++---- packages/cosmwasm-stargate/src/signingcosmwasmclient.ts | 4 ++-- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/packages/cosmwasm-stargate/src/cosmwasmclient.ts b/packages/cosmwasm-stargate/src/cosmwasmclient.ts index 6ae7bb43b6..d95a977b9d 100644 --- a/packages/cosmwasm-stargate/src/cosmwasmclient.ts +++ b/packages/cosmwasm-stargate/src/cosmwasmclient.ts @@ -340,12 +340,10 @@ export class CosmWasmClient { * * If the transaction is broadcasted, a `string` containing the hash of the transaction is returned. The caller then * usually needs to check if the transaction was included in a block and was successful. - * + * * @returns Returns the hash of the transaction */ - public async broadcastTxWithoutPolling( - tx: Uint8Array, - ): Promise { + public async broadcastTxWithoutPolling(tx: Uint8Array): Promise { const broadcasted = await this.forceGetTmClient().broadcastTxSync({ tx }); if (broadcasted.code) { diff --git a/packages/cosmwasm-stargate/src/signingcosmwasmclient.ts b/packages/cosmwasm-stargate/src/signingcosmwasmclient.ts index 8103fcd75a..761bb417b0 100644 --- a/packages/cosmwasm-stargate/src/signingcosmwasmclient.ts +++ b/packages/cosmwasm-stargate/src/signingcosmwasmclient.ts @@ -559,7 +559,7 @@ export class SigningCosmWasmClient extends CosmWasmClient { /** * Creates a transaction with the given messages, fee and memo. Then signs and broadcasts the transaction. - * + * * This method is useful if you want to send a transaction in broadcast, * without waiting for it to be placed inside a block, because for example * I would like to receive the hash to later track the transaction with another tool. @@ -568,7 +568,7 @@ export class SigningCosmWasmClient extends CosmWasmClient { * @param messages * @param fee * @param memo - * + * * @returns Returns the hash of the transaction */ public async signAndBroadcastWithoutPolling( From 4d022115eb7b5137de5b630da587dfc4a433bcd9 Mon Sep 17 00:00:00 2001 From: DavideSegullo Date: Wed, 24 May 2023 18:51:11 +0200 Subject: [PATCH 04/12] refactor: :speech_balloon: change function names changed function names to follow the naming given by the backend. --- packages/cosmwasm-stargate/src/cosmwasmclient.ts | 2 +- packages/cosmwasm-stargate/src/signingcosmwasmclient.ts | 4 ++-- packages/stargate/src/signingstargateclient.ts | 4 ++-- packages/stargate/src/stargateclient.ts | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/cosmwasm-stargate/src/cosmwasmclient.ts b/packages/cosmwasm-stargate/src/cosmwasmclient.ts index d95a977b9d..908ea07704 100644 --- a/packages/cosmwasm-stargate/src/cosmwasmclient.ts +++ b/packages/cosmwasm-stargate/src/cosmwasmclient.ts @@ -343,7 +343,7 @@ export class CosmWasmClient { * * @returns Returns the hash of the transaction */ - public async broadcastTxWithoutPolling(tx: Uint8Array): Promise { + public async broadcastTxSync(tx: Uint8Array): Promise { const broadcasted = await this.forceGetTmClient().broadcastTxSync({ tx }); if (broadcasted.code) { diff --git a/packages/cosmwasm-stargate/src/signingcosmwasmclient.ts b/packages/cosmwasm-stargate/src/signingcosmwasmclient.ts index 761bb417b0..4adbe2e368 100644 --- a/packages/cosmwasm-stargate/src/signingcosmwasmclient.ts +++ b/packages/cosmwasm-stargate/src/signingcosmwasmclient.ts @@ -571,7 +571,7 @@ export class SigningCosmWasmClient extends CosmWasmClient { * * @returns Returns the hash of the transaction */ - public async signAndBroadcastWithoutPolling( + public async signAndBroadcastSync( signerAddress: string, messages: readonly EncodeObject[], fee: StdFee | "auto" | number, @@ -588,7 +588,7 @@ export class SigningCosmWasmClient extends CosmWasmClient { } const txRaw = await this.sign(signerAddress, messages, usedFee, memo); const txBytes = TxRaw.encode(txRaw).finish(); - return this.broadcastTxWithoutPolling(txBytes); + return this.broadcastTxSync(txBytes); } public async sign( diff --git a/packages/stargate/src/signingstargateclient.ts b/packages/stargate/src/signingstargateclient.ts index 84ab551cd7..1ecad0923b 100644 --- a/packages/stargate/src/signingstargateclient.ts +++ b/packages/stargate/src/signingstargateclient.ts @@ -316,7 +316,7 @@ export class SigningStargateClient extends StargateClient { * I would like to receive the hash to later track the transaction with another tool. * @returns Returns the hash of the transaction */ - public async signAndBroadcastWithoutPolling( + public async signAndBroadcastSync( signerAddress: string, messages: readonly EncodeObject[], fee: StdFee | "auto" | number, @@ -333,7 +333,7 @@ export class SigningStargateClient extends StargateClient { } const txRaw = await this.sign(signerAddress, messages, usedFee, memo); const txBytes = TxRaw.encode(txRaw).finish(); - return this.broadcastTxWithoutPolling(txBytes); + return this.broadcastTxSync(txBytes); } /** diff --git a/packages/stargate/src/stargateclient.ts b/packages/stargate/src/stargateclient.ts index 69997d1858..7929a013e2 100644 --- a/packages/stargate/src/stargateclient.ts +++ b/packages/stargate/src/stargateclient.ts @@ -502,7 +502,7 @@ export class StargateClient { * * @returns Returns the hash of the transaction */ - public async broadcastTxWithoutPolling(tx: Uint8Array): Promise { + public async broadcastTxSync(tx: Uint8Array): Promise { const broadcasted = await this.forceGetTmClient().broadcastTxSync({ tx }); if (broadcasted.code) { From 134ecc216f433b3b3c6d4d4f8bd5347465e1b3fc Mon Sep 17 00:00:00 2001 From: DavideSegullo Date: Wed, 24 May 2023 19:04:57 +0200 Subject: [PATCH 05/12] refactor: :recycle: remove duplicated code --- packages/cosmwasm-stargate/src/cosmwasmclient.ts | 9 ++------- packages/stargate/src/stargateclient.ts | 9 ++------- 2 files changed, 4 insertions(+), 14 deletions(-) diff --git a/packages/cosmwasm-stargate/src/cosmwasmclient.ts b/packages/cosmwasm-stargate/src/cosmwasmclient.ts index 908ea07704..90190f105a 100644 --- a/packages/cosmwasm-stargate/src/cosmwasmclient.ts +++ b/packages/cosmwasm-stargate/src/cosmwasmclient.ts @@ -311,13 +311,8 @@ export class CosmWasmClient { : pollForTx(txId); }; - const broadcasted = await this.forceGetTmClient().broadcastTxSync({ tx }); - if (broadcasted.code) { - return Promise.reject( - new BroadcastTxError(broadcasted.code, broadcasted.codespace ?? "", broadcasted.log), - ); - } - const transactionId = toHex(broadcasted.hash).toUpperCase(); + const transactionId = await this.broadcastTxSync(tx); + return new Promise((resolve, reject) => pollForTx(transactionId).then( (value) => { diff --git a/packages/stargate/src/stargateclient.ts b/packages/stargate/src/stargateclient.ts index 7929a013e2..96fc201348 100644 --- a/packages/stargate/src/stargateclient.ts +++ b/packages/stargate/src/stargateclient.ts @@ -470,13 +470,8 @@ export class StargateClient { : pollForTx(txId); }; - const broadcasted = await this.forceGetTmClient().broadcastTxSync({ tx }); - if (broadcasted.code) { - return Promise.reject( - new BroadcastTxError(broadcasted.code, broadcasted.codespace ?? "", broadcasted.log), - ); - } - const transactionId = toHex(broadcasted.hash).toUpperCase(); + const transactionId = await this.broadcastTxSync(tx); + return new Promise((resolve, reject) => pollForTx(transactionId).then( (value) => { From c377c73d0d74c237df34931630916185b5f8024f Mon Sep 17 00:00:00 2001 From: DavideSegullo Date: Thu, 25 May 2023 16:26:34 +0200 Subject: [PATCH 06/12] test: :white_check_mark: add signAndBroadcastSync tests --- .../src/signingstargateclient.spec.ts | 307 ++++++++++++++++++ 1 file changed, 307 insertions(+) diff --git a/packages/stargate/src/signingstargateclient.spec.ts b/packages/stargate/src/signingstargateclient.spec.ts index 2e072f2479..7cadf1eb3c 100644 --- a/packages/stargate/src/signingstargateclient.spec.ts +++ b/packages/stargate/src/signingstargateclient.spec.ts @@ -690,6 +690,313 @@ describe("SigningStargateClient", () => { }); }); + describe("signAndBroadcastSync", () => { + describe("direct mode", () => { + it("works", async () => { + pendingWithoutSimapp(); + const wallet = await DirectSecp256k1HdWallet.fromMnemonic(faucet.mnemonic); + const client = await SigningStargateClient.connectWithSigner( + simapp.tendermintUrl, + wallet, + defaultSigningClientOptions, + ); + + const msg = MsgDelegate.fromPartial({ + delegatorAddress: faucet.address0, + validatorAddress: validator.validatorAddress, + amount: coin(1234, "ustake"), + }); + const msgAny: MsgDelegateEncodeObject = { + typeUrl: "/cosmos.staking.v1beta1.MsgDelegate", + value: msg, + }; + const fee = { + amount: coins(2000, "ucosm"), + gas: "222000", // 222k + }; + const memo = "Use your power wisely"; + const transactionHash = await client.signAndBroadcastSync(faucet.address0, [msgAny], fee, memo); + + expect(transactionHash).toMatch(/^[0-9A-F]{64}$/); + }); + + it("works with auto gas", async () => { + pendingWithoutSimapp(); + const wallet = await DirectSecp256k1HdWallet.fromMnemonic(faucet.mnemonic); + const client = await SigningStargateClient.connectWithSigner(simapp.tendermintUrl, wallet, { + ...defaultSigningClientOptions, + gasPrice: defaultGasPrice, + }); + + const msg = MsgDelegate.fromPartial({ + delegatorAddress: faucet.address0, + validatorAddress: validator.validatorAddress, + amount: coin(1234, "ustake"), + }); + const msgAny: MsgDelegateEncodeObject = { + typeUrl: "/cosmos.staking.v1beta1.MsgDelegate", + value: msg, + }; + const transactionHash = await client.signAndBroadcastSync(faucet.address0, [msgAny], "auto"); + + expect(transactionHash).toMatch(/^[0-9A-F]{64}$/); + }); + + it("works with a modifying signer", async () => { + pendingWithoutSimapp(); + const wallet = await ModifyingDirectSecp256k1HdWallet.fromMnemonic(faucet.mnemonic); + const client = await SigningStargateClient.connectWithSigner( + simapp.tendermintUrl, + wallet, + defaultSigningClientOptions, + ); + + const msg = MsgDelegate.fromPartial({ + delegatorAddress: faucet.address0, + validatorAddress: validator.validatorAddress, + amount: coin(1234, "ustake"), + }); + const msgAny: MsgDelegateEncodeObject = { + typeUrl: "/cosmos.staking.v1beta1.MsgDelegate", + value: msg, + }; + const fee = { + amount: coins(2000, "ucosm"), + gas: "222000", // 222k + }; + const memo = "Use your power wisely"; + const transactionHash = await client.signAndBroadcastSync(faucet.address0, [msgAny], fee, memo); + + expect(transactionHash).toMatch(/^[0-9A-F]{64}$/); + }); + }); + + describe("legacy Amino mode", () => { + it("works with special characters in memo", async () => { + pendingWithoutSimapp(); + const wallet = await Secp256k1HdWallet.fromMnemonic(faucet.mnemonic); + const client = await SigningStargateClient.connectWithSigner( + simapp.tendermintUrl, + wallet, + defaultSigningClientOptions, + ); + + const msgSend: MsgSend = { + fromAddress: faucet.address0, + toAddress: makeRandomAddress(), + amount: coins(1234, "ucosm"), + }; + const msgAny: MsgSendEncodeObject = { + typeUrl: "/cosmos.bank.v1beta1.MsgSend", + value: msgSend, + }; + const fee = { + amount: coins(2000, "ucosm"), + gas: "200000", + }; + const memo = "ampersand:&,lt:<,gt:>"; + const transactionHash = await client.signAndBroadcastSync(faucet.address0, [msgAny], fee, memo); + + expect(transactionHash).toMatch(/^[0-9A-F]{64}$/); + }); + + it("works with bank MsgSend", async () => { + pendingWithoutSimapp(); + const wallet = await Secp256k1HdWallet.fromMnemonic(faucet.mnemonic); + const client = await SigningStargateClient.connectWithSigner( + simapp.tendermintUrl, + wallet, + defaultSigningClientOptions, + ); + + const msgSend: MsgSend = { + fromAddress: faucet.address0, + toAddress: makeRandomAddress(), + amount: coins(1234, "ucosm"), + }; + const msgAny: MsgSendEncodeObject = { + typeUrl: "/cosmos.bank.v1beta1.MsgSend", + value: msgSend, + }; + const fee = { + amount: coins(2000, "ucosm"), + gas: "200000", + }; + const memo = "Use your tokens wisely"; + const transactionHash = await client.signAndBroadcastSync(faucet.address0, [msgAny], fee, memo); + + expect(transactionHash).toMatch(/^[0-9A-F]{64}$/); + }); + + it("works with staking MsgDelegate", async () => { + pendingWithoutSimapp(); + const wallet = await Secp256k1HdWallet.fromMnemonic(faucet.mnemonic); + const client = await SigningStargateClient.connectWithSigner( + simapp.tendermintUrl, + wallet, + defaultSigningClientOptions, + ); + + const msgDelegate: MsgDelegate = { + delegatorAddress: faucet.address0, + validatorAddress: validator.validatorAddress, + amount: coin(1234, "ustake"), + }; + const msgAny: MsgDelegateEncodeObject = { + typeUrl: "/cosmos.staking.v1beta1.MsgDelegate", + value: msgDelegate, + }; + const fee = { + amount: coins(2000, "ustake"), + gas: "200000", + }; + const memo = "Use your tokens wisely"; + const transactionHash = await client.signAndBroadcastSync(faucet.address0, [msgAny], fee, memo); + + expect(transactionHash).toMatch(/^[0-9A-F]{64}$/); + }); + + it("works with a custom registry and custom message", async () => { + pendingWithoutSimapp(); + const wallet = await Secp256k1HdWallet.fromMnemonic(faucet.mnemonic); + + const customRegistry = new Registry(); + const msgDelegateTypeUrl = "/cosmos.staking.v1beta1.MsgDelegate"; + interface CustomMsgDelegate { + customDelegatorAddress?: string; + customValidatorAddress?: string; + customAmount?: Coin; + } + const baseCustomMsgDelegate: CustomMsgDelegate = { + customDelegatorAddress: "", + customValidatorAddress: "", + }; + const CustomMsgDelegate = { + // Adapted from autogenerated MsgDelegate implementation + encode( + message: CustomMsgDelegate, + writer: protobuf.Writer = protobuf.Writer.create(), + ): protobuf.Writer { + writer.uint32(10).string(message.customDelegatorAddress ?? ""); + writer.uint32(18).string(message.customValidatorAddress ?? ""); + if (message.customAmount !== undefined && message.customAmount !== undefined) { + Coin.encode(message.customAmount, writer.uint32(26).fork()).ldelim(); + } + return writer; + }, + + decode(): CustomMsgDelegate { + throw new Error("decode method should not be required"); + }, + + fromPartial(object: DeepPartial): CustomMsgDelegate { + const message = { ...baseCustomMsgDelegate } as CustomMsgDelegate; + if (object.customDelegatorAddress !== undefined && object.customDelegatorAddress !== null) { + message.customDelegatorAddress = object.customDelegatorAddress; + } else { + message.customDelegatorAddress = ""; + } + if (object.customValidatorAddress !== undefined && object.customValidatorAddress !== null) { + message.customValidatorAddress = object.customValidatorAddress; + } else { + message.customValidatorAddress = ""; + } + if (object.customAmount !== undefined && object.customAmount !== null) { + message.customAmount = Coin.fromPartial(object.customAmount); + } else { + message.customAmount = undefined; + } + return message; + }, + }; + customRegistry.register(msgDelegateTypeUrl, CustomMsgDelegate); + const customAminoTypes = new AminoTypes({ + "/cosmos.staking.v1beta1.MsgDelegate": { + aminoType: "cosmos-sdk/MsgDelegate", + toAmino: ({ + customDelegatorAddress, + customValidatorAddress, + customAmount, + }: CustomMsgDelegate): AminoMsgDelegate["value"] => { + assert(customDelegatorAddress, "missing customDelegatorAddress"); + assert(customValidatorAddress, "missing validatorAddress"); + assert(customAmount, "missing amount"); + return { + delegator_address: customDelegatorAddress, + validator_address: customValidatorAddress, + amount: { + amount: customAmount.amount, + denom: customAmount.denom, + }, + }; + }, + fromAmino: ({ + delegator_address, + validator_address, + amount, + }: AminoMsgDelegate["value"]): CustomMsgDelegate => ({ + customDelegatorAddress: delegator_address, + customValidatorAddress: validator_address, + customAmount: Coin.fromPartial(amount), + }), + }, + }); + const options: SigningStargateClientOptions = { + ...defaultSigningClientOptions, + registry: customRegistry, + aminoTypes: customAminoTypes, + }; + const client = await SigningStargateClient.connectWithSigner(simapp.tendermintUrl, wallet, options); + + const msg: CustomMsgDelegate = { + customDelegatorAddress: faucet.address0, + customValidatorAddress: validator.validatorAddress, + customAmount: coin(1234, "ustake"), + }; + const msgAny = { + typeUrl: "/cosmos.staking.v1beta1.MsgDelegate", + value: msg, + }; + const fee = { + amount: coins(2000, "ucosm"), + gas: "200000", + }; + const memo = "Use your power wisely"; + const transactionHash = await client.signAndBroadcastSync(faucet.address0, [msgAny], fee, memo); + + expect(transactionHash).toMatch(/^[0-9A-F]{64}$/); + }); + + it("works with a modifying signer", async () => { + pendingWithoutSimapp(); + const wallet = await ModifyingSecp256k1HdWallet.fromMnemonic(faucet.mnemonic); + const client = await SigningStargateClient.connectWithSigner( + simapp.tendermintUrl, + wallet, + defaultSigningClientOptions, + ); + + const msg: MsgDelegate = { + delegatorAddress: faucet.address0, + validatorAddress: validator.validatorAddress, + amount: coin(1234, "ustake"), + }; + const msgAny: MsgDelegateEncodeObject = { + typeUrl: "/cosmos.staking.v1beta1.MsgDelegate", + value: msg, + }; + const fee = { + amount: coins(2000, "ucosm"), + gas: "200000", + }; + const memo = "Use your power wisely"; + const transactionHash = await client.signAndBroadcastSync(faucet.address0, [msgAny], fee, memo); + + expect(transactionHash).toMatch(/^[0-9A-F]{64}$/); + }); + }); + }); + describe("sign", () => { describe("direct mode", () => { it("works", async () => { From 3d42a4ff5a8ba5be704d8c1fe9d13bdc66e212d1 Mon Sep 17 00:00:00 2001 From: DavideSegullo Date: Thu, 25 May 2023 16:30:37 +0200 Subject: [PATCH 07/12] test: :white_check_mark: add broadcastTxSync test --- packages/stargate/src/stargateclient.spec.ts | 62 ++++++++++++++++++++ 1 file changed, 62 insertions(+) diff --git a/packages/stargate/src/stargateclient.spec.ts b/packages/stargate/src/stargateclient.spec.ts index c7f0df94b2..4dc4c759e0 100644 --- a/packages/stargate/src/stargateclient.spec.ts +++ b/packages/stargate/src/stargateclient.spec.ts @@ -554,4 +554,66 @@ describe("StargateClient", () => { client.disconnect(); }, 30_000); }); + + describe("broadcastTxSync", () => { + it("broadcasts sync a transaction, to get transaction hash", 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 txBodyFields: TxBodyEncodeObject = { + typeUrl: "/cosmos.tx.v1beta1.TxBody", + value: { + messages: [ + { + typeUrl: "/cosmos.bank.v1beta1.MsgSend", + value: { + fromAddress: address, + toAddress: makeRandomAddress(), + 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 feeGranter = undefined; + const feePayer = undefined; + const authInfoBytes = makeAuthInfoBytes( + [{ pubkey, sequence }], + feeAmount, + gasLimit, + feeGranter, + feePayer, + ); + + 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()); + const transactionHash = await client.broadcastTxSync(txRawBytes); + + expect(transactionHash).toMatch(/^[0-9A-F]{64}$/); + + client.disconnect(); + }); + }); }); From 1810853fa4a8966a25e478578e6a5b06a90e9ab9 Mon Sep 17 00:00:00 2001 From: DavideSegullo Date: Thu, 25 May 2023 18:47:59 +0200 Subject: [PATCH 08/12] test: :white_check_mark: add simapp sleep added block time sleep to avoid sequence mismatch. --- .../stargate/src/signingstargateclient.spec.ts | 16 ++++++++++++++++ packages/stargate/src/stargateclient.spec.ts | 2 ++ 2 files changed, 18 insertions(+) diff --git a/packages/stargate/src/signingstargateclient.spec.ts b/packages/stargate/src/signingstargateclient.spec.ts index 7cadf1eb3c..23b8719bca 100644 --- a/packages/stargate/src/signingstargateclient.spec.ts +++ b/packages/stargate/src/signingstargateclient.spec.ts @@ -718,6 +718,8 @@ describe("SigningStargateClient", () => { const transactionHash = await client.signAndBroadcastSync(faucet.address0, [msgAny], fee, memo); expect(transactionHash).toMatch(/^[0-9A-F]{64}$/); + + await sleep(simapp.blockTime * 1.5); }); it("works with auto gas", async () => { @@ -740,6 +742,8 @@ describe("SigningStargateClient", () => { const transactionHash = await client.signAndBroadcastSync(faucet.address0, [msgAny], "auto"); expect(transactionHash).toMatch(/^[0-9A-F]{64}$/); + + await sleep(simapp.blockTime * 1.5); }); it("works with a modifying signer", async () => { @@ -768,6 +772,8 @@ describe("SigningStargateClient", () => { const transactionHash = await client.signAndBroadcastSync(faucet.address0, [msgAny], fee, memo); expect(transactionHash).toMatch(/^[0-9A-F]{64}$/); + + await sleep(simapp.blockTime * 1.5); }); }); @@ -798,6 +804,8 @@ describe("SigningStargateClient", () => { const transactionHash = await client.signAndBroadcastSync(faucet.address0, [msgAny], fee, memo); expect(transactionHash).toMatch(/^[0-9A-F]{64}$/); + + await sleep(simapp.blockTime * 1.5); }); it("works with bank MsgSend", async () => { @@ -826,6 +834,8 @@ describe("SigningStargateClient", () => { const transactionHash = await client.signAndBroadcastSync(faucet.address0, [msgAny], fee, memo); expect(transactionHash).toMatch(/^[0-9A-F]{64}$/); + + await sleep(simapp.blockTime * 1.5); }); it("works with staking MsgDelegate", async () => { @@ -854,6 +864,8 @@ describe("SigningStargateClient", () => { const transactionHash = await client.signAndBroadcastSync(faucet.address0, [msgAny], fee, memo); expect(transactionHash).toMatch(/^[0-9A-F]{64}$/); + + await sleep(simapp.blockTime * 1.5); }); it("works with a custom registry and custom message", async () => { @@ -965,6 +977,8 @@ describe("SigningStargateClient", () => { const transactionHash = await client.signAndBroadcastSync(faucet.address0, [msgAny], fee, memo); expect(transactionHash).toMatch(/^[0-9A-F]{64}$/); + + await sleep(simapp.blockTime * 1.5); }); it("works with a modifying signer", async () => { @@ -993,6 +1007,8 @@ describe("SigningStargateClient", () => { const transactionHash = await client.signAndBroadcastSync(faucet.address0, [msgAny], fee, memo); expect(transactionHash).toMatch(/^[0-9A-F]{64}$/); + + await sleep(simapp.blockTime * 1.5); }); }); }); diff --git a/packages/stargate/src/stargateclient.spec.ts b/packages/stargate/src/stargateclient.spec.ts index 4dc4c759e0..7636efdf33 100644 --- a/packages/stargate/src/stargateclient.spec.ts +++ b/packages/stargate/src/stargateclient.spec.ts @@ -613,6 +613,8 @@ describe("StargateClient", () => { expect(transactionHash).toMatch(/^[0-9A-F]{64}$/); + await sleep(simapp.blockTime * 1.5); + client.disconnect(); }); }); From 3cc3c4f4c20d3949aa396a4e2a788f8b1a9cd843 Mon Sep 17 00:00:00 2001 From: DavideSegullo Date: Thu, 25 May 2023 22:46:47 +0200 Subject: [PATCH 09/12] test: :white_check_mark: change signAndBroadcastSync test changed test msg from delegate to send. --- .../src/signingstargateclient.spec.ts | 52 ++++++++++--------- 1 file changed, 28 insertions(+), 24 deletions(-) diff --git a/packages/stargate/src/signingstargateclient.spec.ts b/packages/stargate/src/signingstargateclient.spec.ts index 23b8719bca..ab05235793 100644 --- a/packages/stargate/src/signingstargateclient.spec.ts +++ b/packages/stargate/src/signingstargateclient.spec.ts @@ -701,14 +701,15 @@ describe("SigningStargateClient", () => { defaultSigningClientOptions, ); - const msg = MsgDelegate.fromPartial({ - delegatorAddress: faucet.address0, - validatorAddress: validator.validatorAddress, - amount: coin(1234, "ustake"), - }); - const msgAny: MsgDelegateEncodeObject = { - typeUrl: "/cosmos.staking.v1beta1.MsgDelegate", - value: msg, + const msgSend: MsgSend = { + fromAddress: faucet.address0, + toAddress: makeRandomAddress(), + amount: coins(1234, "ucosm"), + }; + + const msgAny: MsgSendEncodeObject = { + typeUrl: "/cosmos.bank.v1beta1.MsgSend", + value: msgSend, }; const fee = { amount: coins(2000, "ucosm"), @@ -730,14 +731,15 @@ describe("SigningStargateClient", () => { gasPrice: defaultGasPrice, }); - const msg = MsgDelegate.fromPartial({ - delegatorAddress: faucet.address0, - validatorAddress: validator.validatorAddress, - amount: coin(1234, "ustake"), - }); - const msgAny: MsgDelegateEncodeObject = { - typeUrl: "/cosmos.staking.v1beta1.MsgDelegate", - value: msg, + const msgSend: MsgSend = { + fromAddress: faucet.address0, + toAddress: makeRandomAddress(), + amount: coins(1234, "ucosm"), + }; + + const msgAny: MsgSendEncodeObject = { + typeUrl: "/cosmos.bank.v1beta1.MsgSend", + value: msgSend, }; const transactionHash = await client.signAndBroadcastSync(faucet.address0, [msgAny], "auto"); @@ -755,15 +757,17 @@ describe("SigningStargateClient", () => { defaultSigningClientOptions, ); - const msg = MsgDelegate.fromPartial({ - delegatorAddress: faucet.address0, - validatorAddress: validator.validatorAddress, - amount: coin(1234, "ustake"), - }); - const msgAny: MsgDelegateEncodeObject = { - typeUrl: "/cosmos.staking.v1beta1.MsgDelegate", - value: msg, + const msgSend: MsgSend = { + fromAddress: faucet.address0, + toAddress: makeRandomAddress(), + amount: coins(1234, "ucosm"), }; + + const msgAny: MsgSendEncodeObject = { + typeUrl: "/cosmos.bank.v1beta1.MsgSend", + value: msgSend, + }; + const fee = { amount: coins(2000, "ucosm"), gas: "222000", // 222k From 9c1a87172204655cd58c4d90f011ff7e26f7e63d Mon Sep 17 00:00:00 2001 From: Davide Segullo Date: Wed, 14 Jun 2023 10:01:51 +0200 Subject: [PATCH 10/12] Update packages/stargate/src/signingstargateclient.spec.ts Co-authored-by: Simon Warta <2603011+webmaster128@users.noreply.github.com> --- .../src/signingstargateclient.spec.ts | 173 ------------------ 1 file changed, 173 deletions(-) diff --git a/packages/stargate/src/signingstargateclient.spec.ts b/packages/stargate/src/signingstargateclient.spec.ts index ab05235793..31a318f0e4 100644 --- a/packages/stargate/src/signingstargateclient.spec.ts +++ b/packages/stargate/src/signingstargateclient.spec.ts @@ -841,179 +841,6 @@ describe("SigningStargateClient", () => { await sleep(simapp.blockTime * 1.5); }); - - it("works with staking MsgDelegate", async () => { - pendingWithoutSimapp(); - const wallet = await Secp256k1HdWallet.fromMnemonic(faucet.mnemonic); - const client = await SigningStargateClient.connectWithSigner( - simapp.tendermintUrl, - wallet, - defaultSigningClientOptions, - ); - - const msgDelegate: MsgDelegate = { - delegatorAddress: faucet.address0, - validatorAddress: validator.validatorAddress, - amount: coin(1234, "ustake"), - }; - const msgAny: MsgDelegateEncodeObject = { - typeUrl: "/cosmos.staking.v1beta1.MsgDelegate", - value: msgDelegate, - }; - const fee = { - amount: coins(2000, "ustake"), - gas: "200000", - }; - const memo = "Use your tokens wisely"; - const transactionHash = await client.signAndBroadcastSync(faucet.address0, [msgAny], fee, memo); - - expect(transactionHash).toMatch(/^[0-9A-F]{64}$/); - - await sleep(simapp.blockTime * 1.5); - }); - - it("works with a custom registry and custom message", async () => { - pendingWithoutSimapp(); - const wallet = await Secp256k1HdWallet.fromMnemonic(faucet.mnemonic); - - const customRegistry = new Registry(); - const msgDelegateTypeUrl = "/cosmos.staking.v1beta1.MsgDelegate"; - interface CustomMsgDelegate { - customDelegatorAddress?: string; - customValidatorAddress?: string; - customAmount?: Coin; - } - const baseCustomMsgDelegate: CustomMsgDelegate = { - customDelegatorAddress: "", - customValidatorAddress: "", - }; - const CustomMsgDelegate = { - // Adapted from autogenerated MsgDelegate implementation - encode( - message: CustomMsgDelegate, - writer: protobuf.Writer = protobuf.Writer.create(), - ): protobuf.Writer { - writer.uint32(10).string(message.customDelegatorAddress ?? ""); - writer.uint32(18).string(message.customValidatorAddress ?? ""); - if (message.customAmount !== undefined && message.customAmount !== undefined) { - Coin.encode(message.customAmount, writer.uint32(26).fork()).ldelim(); - } - return writer; - }, - - decode(): CustomMsgDelegate { - throw new Error("decode method should not be required"); - }, - - fromPartial(object: DeepPartial): CustomMsgDelegate { - const message = { ...baseCustomMsgDelegate } as CustomMsgDelegate; - if (object.customDelegatorAddress !== undefined && object.customDelegatorAddress !== null) { - message.customDelegatorAddress = object.customDelegatorAddress; - } else { - message.customDelegatorAddress = ""; - } - if (object.customValidatorAddress !== undefined && object.customValidatorAddress !== null) { - message.customValidatorAddress = object.customValidatorAddress; - } else { - message.customValidatorAddress = ""; - } - if (object.customAmount !== undefined && object.customAmount !== null) { - message.customAmount = Coin.fromPartial(object.customAmount); - } else { - message.customAmount = undefined; - } - return message; - }, - }; - customRegistry.register(msgDelegateTypeUrl, CustomMsgDelegate); - const customAminoTypes = new AminoTypes({ - "/cosmos.staking.v1beta1.MsgDelegate": { - aminoType: "cosmos-sdk/MsgDelegate", - toAmino: ({ - customDelegatorAddress, - customValidatorAddress, - customAmount, - }: CustomMsgDelegate): AminoMsgDelegate["value"] => { - assert(customDelegatorAddress, "missing customDelegatorAddress"); - assert(customValidatorAddress, "missing validatorAddress"); - assert(customAmount, "missing amount"); - return { - delegator_address: customDelegatorAddress, - validator_address: customValidatorAddress, - amount: { - amount: customAmount.amount, - denom: customAmount.denom, - }, - }; - }, - fromAmino: ({ - delegator_address, - validator_address, - amount, - }: AminoMsgDelegate["value"]): CustomMsgDelegate => ({ - customDelegatorAddress: delegator_address, - customValidatorAddress: validator_address, - customAmount: Coin.fromPartial(amount), - }), - }, - }); - const options: SigningStargateClientOptions = { - ...defaultSigningClientOptions, - registry: customRegistry, - aminoTypes: customAminoTypes, - }; - const client = await SigningStargateClient.connectWithSigner(simapp.tendermintUrl, wallet, options); - - const msg: CustomMsgDelegate = { - customDelegatorAddress: faucet.address0, - customValidatorAddress: validator.validatorAddress, - customAmount: coin(1234, "ustake"), - }; - const msgAny = { - typeUrl: "/cosmos.staking.v1beta1.MsgDelegate", - value: msg, - }; - const fee = { - amount: coins(2000, "ucosm"), - gas: "200000", - }; - const memo = "Use your power wisely"; - const transactionHash = await client.signAndBroadcastSync(faucet.address0, [msgAny], fee, memo); - - expect(transactionHash).toMatch(/^[0-9A-F]{64}$/); - - await sleep(simapp.blockTime * 1.5); - }); - - it("works with a modifying signer", async () => { - pendingWithoutSimapp(); - const wallet = await ModifyingSecp256k1HdWallet.fromMnemonic(faucet.mnemonic); - const client = await SigningStargateClient.connectWithSigner( - simapp.tendermintUrl, - wallet, - defaultSigningClientOptions, - ); - - const msg: MsgDelegate = { - delegatorAddress: faucet.address0, - validatorAddress: validator.validatorAddress, - amount: coin(1234, "ustake"), - }; - const msgAny: MsgDelegateEncodeObject = { - typeUrl: "/cosmos.staking.v1beta1.MsgDelegate", - value: msg, - }; - const fee = { - amount: coins(2000, "ucosm"), - gas: "200000", - }; - const memo = "Use your power wisely"; - const transactionHash = await client.signAndBroadcastSync(faucet.address0, [msgAny], fee, memo); - - expect(transactionHash).toMatch(/^[0-9A-F]{64}$/); - - await sleep(simapp.blockTime * 1.5); - }); }); }); From e37b6149e2617a7f5ec0874931b67a5aa922ec26 Mon Sep 17 00:00:00 2001 From: DavideSegullo Date: Wed, 14 Jun 2023 16:20:33 +0200 Subject: [PATCH 11/12] style: :rotating_light: fix lint errors --- .../src/signingstargateclient.spec.ts | 26 +++++++++---------- packages/stargate/src/stargateclient.spec.ts | 4 +-- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/packages/stargate/src/signingstargateclient.spec.ts b/packages/stargate/src/signingstargateclient.spec.ts index 31a318f0e4..b76abc3600 100644 --- a/packages/stargate/src/signingstargateclient.spec.ts +++ b/packages/stargate/src/signingstargateclient.spec.ts @@ -706,7 +706,7 @@ describe("SigningStargateClient", () => { toAddress: makeRandomAddress(), amount: coins(1234, "ucosm"), }; - + const msgAny: MsgSendEncodeObject = { typeUrl: "/cosmos.bank.v1beta1.MsgSend", value: msgSend, @@ -717,10 +717,10 @@ describe("SigningStargateClient", () => { }; const memo = "Use your power wisely"; const transactionHash = await client.signAndBroadcastSync(faucet.address0, [msgAny], fee, memo); - + expect(transactionHash).toMatch(/^[0-9A-F]{64}$/); - await sleep(simapp.blockTime * 1.5); + await sleep(simapp.blockTime * 1.5); }); it("works with auto gas", async () => { @@ -736,16 +736,16 @@ describe("SigningStargateClient", () => { toAddress: makeRandomAddress(), amount: coins(1234, "ucosm"), }; - + const msgAny: MsgSendEncodeObject = { typeUrl: "/cosmos.bank.v1beta1.MsgSend", value: msgSend, }; const transactionHash = await client.signAndBroadcastSync(faucet.address0, [msgAny], "auto"); - + expect(transactionHash).toMatch(/^[0-9A-F]{64}$/); - await sleep(simapp.blockTime * 1.5); + await sleep(simapp.blockTime * 1.5); }); it("works with a modifying signer", async () => { @@ -762,7 +762,7 @@ describe("SigningStargateClient", () => { toAddress: makeRandomAddress(), amount: coins(1234, "ucosm"), }; - + const msgAny: MsgSendEncodeObject = { typeUrl: "/cosmos.bank.v1beta1.MsgSend", value: msgSend, @@ -774,10 +774,10 @@ describe("SigningStargateClient", () => { }; const memo = "Use your power wisely"; const transactionHash = await client.signAndBroadcastSync(faucet.address0, [msgAny], fee, memo); - + expect(transactionHash).toMatch(/^[0-9A-F]{64}$/); - await sleep(simapp.blockTime * 1.5); + await sleep(simapp.blockTime * 1.5); }); }); @@ -806,10 +806,10 @@ describe("SigningStargateClient", () => { }; const memo = "ampersand:&,lt:<,gt:>"; const transactionHash = await client.signAndBroadcastSync(faucet.address0, [msgAny], fee, memo); - + expect(transactionHash).toMatch(/^[0-9A-F]{64}$/); - await sleep(simapp.blockTime * 1.5); + await sleep(simapp.blockTime * 1.5); }); it("works with bank MsgSend", async () => { @@ -836,10 +836,10 @@ describe("SigningStargateClient", () => { }; const memo = "Use your tokens wisely"; const transactionHash = await client.signAndBroadcastSync(faucet.address0, [msgAny], fee, memo); - + expect(transactionHash).toMatch(/^[0-9A-F]{64}$/); - await sleep(simapp.blockTime * 1.5); + await sleep(simapp.blockTime * 1.5); }); }); }); diff --git a/packages/stargate/src/stargateclient.spec.ts b/packages/stargate/src/stargateclient.spec.ts index 7636efdf33..52b97eb80c 100644 --- a/packages/stargate/src/stargateclient.spec.ts +++ b/packages/stargate/src/stargateclient.spec.ts @@ -610,10 +610,10 @@ describe("StargateClient", () => { }); const txRawBytes = Uint8Array.from(TxRaw.encode(txRaw).finish()); const transactionHash = await client.broadcastTxSync(txRawBytes); - + expect(transactionHash).toMatch(/^[0-9A-F]{64}$/); - await sleep(simapp.blockTime * 1.5); + await sleep(simapp.blockTime * 1.5); client.disconnect(); }); From 0701bac652ab8e6a27e94191423cd64f48cff290 Mon Sep 17 00:00:00 2001 From: Simon Warta Date: Mon, 19 Jun 2023 15:01:20 +0200 Subject: [PATCH 12/12] Add CHANGELOG entry --- CHANGELOG.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index bb059965e6..c1102f0565 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,8 +11,15 @@ and this project adheres to - @cosmjs/cosmwasm-stargate: Add `SigningCosmWasmClient.instantiate2` ([#1407]). - @cosmjs/stargate: `IndexedTx` and `DeliverTxResponse` now have a `msgResponses` field ([#1305]). +- @cosmjs/cosmwasm-stargate: Add `CosmWasmClient.broadcastTxSync` and + `SigningCosmWasmClient.signAndBroadcastSync` to allow broadcasting without + waiting for block inclusion. ([#1396]) +- @cosmjs/stargateAdd `StargateClient.broadcastTxSync` and + `SigningStargateClient.signAndBroadcastSync` to allow broadcasting without + waiting for block inclusion. ([#1396]) [#1305]: https://github.com/cosmos/cosmjs/issues/1305 +[#1396]: https://github.com/cosmos/cosmjs/pull/1396 [#1407]: https://github.com/cosmos/cosmjs/pull/1407 ### Changed