Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve DX - Stargate sign and broadcast #1396

Merged
merged 12 commits into from
Jun 19, 2023
Merged
34 changes: 27 additions & 7 deletions packages/cosmwasm-stargate/src/cosmwasmclient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -295,13 +295,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) => {
Expand All @@ -316,6 +311,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.
*
Expand Down
34 changes: 34 additions & 0 deletions packages/cosmwasm-stargate/src/signingcosmwasmclient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -611,6 +611,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[],
Expand Down
154 changes: 154 additions & 0 deletions packages/stargate/src/signingstargateclient.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
};
webmaster128 marked this conversation as resolved.
Show resolved Hide resolved
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);
});
webmaster128 marked this conversation as resolved.
Show resolved Hide resolved

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 () => {
Expand Down
26 changes: 26 additions & 0 deletions packages/stargate/src/signingstargateclient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
64 changes: 64 additions & 0 deletions packages/stargate/src/stargateclient.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
});
});
});
34 changes: 27 additions & 7 deletions packages/stargate/src/stargateclient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) => {
Expand All @@ -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 => {
Expand Down