Merge branch 'main' into issue334/implement_instantiate_permission

This commit is contained in:
Simon Warta 2023-06-19 15:11:48 +02:00
commit 32c4057e1e
7 changed files with 339 additions and 14 deletions

View File

@ -20,9 +20,16 @@ and this project adheres to
([#1266]).
- @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])
[#1266]: https://github.com/cosmos/cosmjs/issues/1266
[#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

View File

@ -296,13 +296,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) => {
@ -317,6 +312,31 @@ 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 broadcastTxSync(tx: Uint8Array): Promise<string> {
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.
*

View File

@ -614,6 +614,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 signAndBroadcastSync(
signerAddress: string,
messages: readonly EncodeObject[],
fee: StdFee | "auto" | number,
memo = "",
): Promise<string> {
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.broadcastTxSync(txBytes);
}
public async sign(
signerAddress: string,
messages: readonly EncodeObject[],

View File

@ -690,6 +690,160 @@ 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 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
};
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 auto gas", async () => {
pendingWithoutSimapp();
const wallet = await DirectSecp256k1HdWallet.fromMnemonic(faucet.mnemonic);
const client = await SigningStargateClient.connectWithSigner(simapp.tendermintUrl, wallet, {
...defaultSigningClientOptions,
gasPrice: defaultGasPrice,
});
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");
expect(transactionHash).toMatch(/^[0-9A-F]{64}$/);
await sleep(simapp.blockTime * 1.5);
});
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 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
};
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);
});
});
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}$/);
await sleep(simapp.blockTime * 1.5);
});
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}$/);
await sleep(simapp.blockTime * 1.5);
});
});
});
describe("sign", () => {
describe("direct mode", () => {
it("works", async () => {

View File

@ -326,6 +326,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 signAndBroadcastSync(
signerAddress: string,
messages: readonly EncodeObject[],
fee: StdFee | "auto" | number,
memo = "",
): Promise<string> {
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.broadcastTxSync(txBytes);
}
/**
* Gets account number and sequence from the API, creates a sign doc,
* creates a single signature and assembles the signed transaction.

View File

@ -554,4 +554,68 @@ 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}$/);
await sleep(simapp.blockTime * 1.5);
client.disconnect();
});
});
});

View File

@ -464,13 +464,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) => {
@ -485,6 +480,31 @@ 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 broadcastTxSync(tx: Uint8Array): Promise<string> {
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<IndexedTx[]> {
const results = await this.forceGetTmClient().txSearchAll({ query: query });
return results.txs.map((tx): IndexedTx => {