Merge pull request #931 from cosmos/add-simulate

Add simulation support
This commit is contained in:
Simon Warta 2021-11-22 18:54:36 +01:00 committed by GitHub
commit 0d9108e3a1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 553 additions and 45 deletions

View File

@ -0,0 +1,123 @@
import { coins, makeCosmoshubPath } from "@cosmjs/amino";
import { DirectSecp256k1HdWallet } from "@cosmjs/proto-signing";
import {
assertIsBroadcastTxSuccess,
calculateFee,
GasPrice,
MsgSendEncodeObject,
SigningStargateClient,
} from "@cosmjs/stargate";
// Wallet
const mnemonic =
"economy stock theory fatal elder harbor betray wasp final emotion task crumble siren bottom lizard educate guess current outdoor pair theory focus wife stone";
const path = makeCosmoshubPath(3);
const prefix = "cosmos";
const wallet = await DirectSecp256k1HdWallet.fromMnemonic(mnemonic, { hdPaths: [path], prefix: prefix });
const [account] = await wallet.getAccounts();
console.log("Signer address:", account.address);
// Network config
const rpcEndpoint = "ws://localhost:26658";
const gasPrice = GasPrice.fromString("0.025ucosm");
// Setup client
const client = await SigningStargateClient.connectWithSigner(rpcEndpoint, wallet, { gasPrice: gasPrice });
// Send transaction (using sendTokens with auto gas)
{
const recipient = "cosmos1xv9tklw7d82sezh9haa573wufgy59vmwe6xxe5";
const amount = coins(1234567, "ucosm");
const memo = "With simulate";
const result = await client.sendTokens(account.address, recipient, amount, "auto", memo);
assertIsBroadcastTxSuccess(result);
console.log("Successfully broadcasted:", result);
}
// Send transaction (using sendTokens with auto gas and custom muliplier)
{
const recipient = "cosmos1xv9tklw7d82sezh9haa573wufgy59vmwe6xxe5";
const amount = coins(1234567, "ucosm");
const memo = "With simulate";
const result = await client.sendTokens(account.address, recipient, amount, 1.2, memo);
assertIsBroadcastTxSuccess(result);
console.log("Successfully broadcasted:", result);
}
// Send transaction (using sendTokens with manual gas)
{
const recipient = "cosmos1xv9tklw7d82sezh9haa573wufgy59vmwe6xxe5";
const amount = coins(1234567, "ucosm");
const sendMsg: MsgSendEncodeObject = {
typeUrl: "/cosmos.bank.v1beta1.MsgSend",
value: {
fromAddress: account.address,
toAddress: recipient,
amount: amount,
},
};
const memo = "With simulate";
const gasEstimation = await client.simulate(account.address, [sendMsg], memo);
const fee = calculateFee(Math.round(gasEstimation * 1.3), gasPrice);
const result = await client.sendTokens(account.address, recipient, amount, fee, memo);
assertIsBroadcastTxSuccess(result);
console.log("Successfully broadcasted:", result);
}
// Send transaction (using signAndBroadcast with auto gas)
{
const recipient = "cosmos1xv9tklw7d82sezh9haa573wufgy59vmwe6xxe5";
const amount = coins(1234567, "ucosm");
const sendMsg: MsgSendEncodeObject = {
typeUrl: "/cosmos.bank.v1beta1.MsgSend",
value: {
fromAddress: account.address,
toAddress: recipient,
amount: amount,
},
};
const memo = "With simulate";
const result = await client.signAndBroadcast(account.address, [sendMsg], "auto", memo);
assertIsBroadcastTxSuccess(result);
console.log("Successfully broadcasted:", result);
}
// Send transaction (using signAndBroadcast with auto gas and custom muliplier)
{
const recipient = "cosmos1xv9tklw7d82sezh9haa573wufgy59vmwe6xxe5";
const amount = coins(1234567, "ucosm");
const sendMsg: MsgSendEncodeObject = {
typeUrl: "/cosmos.bank.v1beta1.MsgSend",
value: {
fromAddress: account.address,
toAddress: recipient,
amount: amount,
},
};
const memo = "With simulate";
const result = await client.signAndBroadcast(account.address, [sendMsg], 1.4, memo);
assertIsBroadcastTxSuccess(result);
console.log("Successfully broadcasted:", result);
}
// Send transaction (using signAndBroadcast with manual gas)
{
const recipient = "cosmos1xv9tklw7d82sezh9haa573wufgy59vmwe6xxe5";
const amount = coins(1234567, "ucosm");
const sendMsg: MsgSendEncodeObject = {
typeUrl: "/cosmos.bank.v1beta1.MsgSend",
value: {
fromAddress: account.address,
toAddress: recipient,
amount: amount,
},
};
const memo = "With simulate";
const gasEstimation = await client.simulate(account.address, [sendMsg], memo);
const fee = calculateFee(Math.round(gasEstimation * 1.3), gasPrice);
const result = await client.signAndBroadcast(account.address, [sendMsg], fee, memo);
assertIsBroadcastTxSuccess(result);
console.log("Successfully broadcasted:", result);
}
client.disconnect();

View File

@ -1,4 +1,4 @@
import { makeCosmoshubPath } from "@cosmjs/amino";
import { coins, makeCosmoshubPath } from "@cosmjs/amino";
import { DirectSecp256k1HdWallet } from "@cosmjs/proto-signing";
import { assertIsBroadcastTxSuccess, calculateFee, GasPrice, SigningStargateClient } from "@cosmjs/stargate";
@ -20,15 +20,12 @@ const client = await SigningStargateClient.connectWithSigner(rpcEndpoint, wallet
// Send transaction
const recipient = "cosmos1xv9tklw7d82sezh9haa573wufgy59vmwe6xxe5";
const amount = {
denom: "ucosm",
amount: "1234567",
};
const amount = coins(1234567, "ucosm");
const fee = calculateFee(200_000, gasPrice);
const result = await client.sendTokens(
account.address,
recipient,
[amount],
amount,
fee,
"Have fun with your star coins",
);

View File

@ -15,4 +15,5 @@ yarn node ./bin/cosmwasm-cli --init examples/mask.ts --code "process.exit(0)"
yarn node ./bin/cosmwasm-cli --init examples/multisig_address.ts --code "process.exit(0)"
if [ -n "${SIMAPP42_ENABLED:-}" ]; then
yarn node ./bin/cosmwasm-cli --init examples/stargate.ts --code "process.exit(0)"
yarn node ./bin/cosmwasm-cli --init examples/simulate.ts --code "process.exit(0)"
fi

View File

@ -19,7 +19,9 @@ import {
SequenceResponse,
setupAuthExtension,
setupBankExtension,
setupTxExtension,
TimeoutError,
TxExtension,
} from "@cosmjs/stargate";
import { Tendermint34Client, toRfc3339WithNanoseconds } from "@cosmjs/tendermint-rpc";
import { assert, sleep } from "@cosmjs/utils";
@ -75,12 +77,16 @@ export interface ContractCodeHistoryEntry {
/** Use for testing only */
export interface PrivateCosmWasmClient {
readonly tmClient: Tendermint34Client | undefined;
readonly queryClient: (QueryClient & AuthExtension & BankExtension & WasmExtension) | undefined;
readonly queryClient:
| (QueryClient & AuthExtension & BankExtension & TxExtension & WasmExtension)
| undefined;
}
export class CosmWasmClient {
private readonly tmClient: Tendermint34Client | undefined;
private readonly queryClient: (QueryClient & AuthExtension & BankExtension & WasmExtension) | undefined;
private readonly queryClient:
| (QueryClient & AuthExtension & BankExtension & TxExtension & WasmExtension)
| undefined;
private readonly codesCache = new Map<number, CodeDetails>();
private chainId: string | undefined;
@ -97,6 +103,7 @@ export class CosmWasmClient {
setupAuthExtension,
setupBankExtension,
setupWasmExtension,
setupTxExtension,
);
}
}
@ -114,11 +121,13 @@ export class CosmWasmClient {
return this.tmClient;
}
protected getQueryClient(): (QueryClient & AuthExtension & BankExtension & WasmExtension) | undefined {
protected getQueryClient():
| (QueryClient & AuthExtension & BankExtension & TxExtension & WasmExtension)
| undefined {
return this.queryClient;
}
protected forceGetQueryClient(): QueryClient & AuthExtension & BankExtension & WasmExtension {
protected forceGetQueryClient(): QueryClient & AuthExtension & BankExtension & TxExtension & WasmExtension {
if (!this.queryClient) {
throw new Error("Query client not available. You cannot use online functionality in offline mode.");
}

View File

@ -1,7 +1,7 @@
/* eslint-disable @typescript-eslint/naming-convention */
import { Secp256k1HdWallet } from "@cosmjs/amino";
import { sha256 } from "@cosmjs/crypto";
import { toHex } from "@cosmjs/encoding";
import { toHex, toUtf8 } from "@cosmjs/encoding";
import { decodeTxRaw, DirectSecp256k1HdWallet, Registry } from "@cosmjs/proto-signing";
import {
AminoMsgDelegate,
@ -17,17 +17,18 @@ import { DeepPartial, MsgSend } from "cosmjs-types/cosmos/bank/v1beta1/tx";
import { Coin } from "cosmjs-types/cosmos/base/v1beta1/coin";
import { MsgDelegate } from "cosmjs-types/cosmos/staking/v1beta1/tx";
import { AuthInfo, TxBody, TxRaw } from "cosmjs-types/cosmos/tx/v1beta1/tx";
import { MsgStoreCode } from "cosmjs-types/cosmwasm/wasm/v1/tx";
import { MsgExecuteContract, MsgStoreCode } from "cosmjs-types/cosmwasm/wasm/v1/tx";
import Long from "long";
import pako from "pako";
import protobuf from "protobufjs/minimal";
import { MsgStoreCodeEncodeObject } from "./encodeobjects";
import { MsgExecuteContractEncodeObject, MsgStoreCodeEncodeObject } from "./encodeobjects";
import { SigningCosmWasmClient } from "./signingcosmwasmclient";
import {
alice,
defaultClearAdminFee,
defaultExecuteFee,
defaultGasPrice,
defaultInstantiateFee,
defaultMigrateFee,
defaultSendFee,
@ -71,6 +72,30 @@ describe("SigningCosmWasmClient", () => {
});
});
describe("simulate", () => {
it("works", async () => {
pendingWithoutWasmd();
const wallet = await DirectSecp256k1HdWallet.fromMnemonic(alice.mnemonic, { prefix: wasmd.prefix });
const options = { ...defaultSigningClientOptions, prefix: wasmd.prefix };
const client = await SigningCosmWasmClient.connectWithSigner(wasmd.endpoint, wallet, options);
const executeContractMsg: MsgExecuteContractEncodeObject = {
typeUrl: "/cosmwasm.wasm.v1.MsgExecuteContract",
value: MsgExecuteContract.fromPartial({
sender: alice.address0,
contract: deployedHackatom.instances[0].address,
msg: toUtf8(`{"release":{}}`),
funds: [],
}),
};
const memo = "Go go go";
const gasUsed = await client.simulate(alice.address0, [executeContractMsg], memo);
expect(gasUsed).toBeGreaterThanOrEqual(101_000);
expect(gasUsed).toBeLessThanOrEqual(150_000);
client.disconnect();
});
});
describe("upload", () => {
it("works", async () => {
pendingWithoutWasmd();
@ -576,6 +601,32 @@ describe("SigningCosmWasmClient", () => {
client.disconnect();
});
it("works with auto gas", async () => {
pendingWithoutWasmd();
const wallet = await DirectSecp256k1HdWallet.fromMnemonic(alice.mnemonic, { prefix: wasmd.prefix });
const client = await SigningCosmWasmClient.connectWithSigner(wasmd.endpoint, wallet, {
...defaultSigningClientOptions,
prefix: wasmd.prefix,
gasPrice: defaultGasPrice,
});
const msgDelegateTypeUrl = "/cosmos.staking.v1beta1.MsgDelegate";
const msg = MsgDelegate.fromPartial({
delegatorAddress: alice.address0,
validatorAddress: validator.validatorAddress,
amount: coin(1234, "ustake"),
});
const msgAny: MsgDelegateEncodeObject = {
typeUrl: msgDelegateTypeUrl,
value: msg,
};
const memo = "Use your power wisely";
const result = await client.signAndBroadcast(alice.address0, [msgAny], "auto", memo);
assertIsBroadcastTxSuccess(result);
client.disconnect();
});
it("works with a modifying signer", async () => {
pendingWithoutWasmd();
const wallet = await ModifyingDirectSecp256k1HdWallet.fromMnemonic(alice.mnemonic, {

View File

@ -17,8 +17,10 @@ import {
AminoTypes,
BroadcastTxFailure,
BroadcastTxResponse,
calculateFee,
Coin,
defaultRegistryTypes,
GasPrice,
isBroadcastTxFailure,
logs,
MsgDelegateEncodeObject,
@ -29,7 +31,7 @@ import {
StdFee,
} from "@cosmjs/stargate";
import { Tendermint34Client } from "@cosmjs/tendermint-rpc";
import { assert } from "@cosmjs/utils";
import { assert, assertDefined } from "@cosmjs/utils";
import { MsgWithdrawDelegatorReward } from "cosmjs-types/cosmos/distribution/v1beta1/tx";
import { MsgDelegate, MsgUndelegate } from "cosmjs-types/cosmos/staking/v1beta1/tx";
import { SignMode } from "cosmjs-types/cosmos/tx/signing/v1beta1/signing";
@ -144,6 +146,7 @@ export interface SigningCosmWasmClientOptions {
readonly prefix?: string;
readonly broadcastTimeoutMs?: number;
readonly broadcastPollIntervalMs?: number;
readonly gasPrice?: GasPrice;
}
export class SigningCosmWasmClient extends CosmWasmClient {
@ -153,6 +156,7 @@ export class SigningCosmWasmClient extends CosmWasmClient {
private readonly signer: OfflineSigner;
private readonly aminoTypes: AminoTypes;
private readonly gasPrice: GasPrice | undefined;
public static async connectWithSigner(
endpoint: string,
@ -194,13 +198,33 @@ export class SigningCosmWasmClient extends CosmWasmClient {
this.signer = signer;
this.broadcastTimeoutMs = options.broadcastTimeoutMs;
this.broadcastPollIntervalMs = options.broadcastPollIntervalMs;
this.gasPrice = options.gasPrice;
}
public async simulate(
signerAddress: string,
messages: readonly EncodeObject[],
memo: string | undefined,
): Promise<number> {
const anyMsgs = messages.map((m) => this.registry.encodeAsAny(m));
const accountFromSigner = (await this.signer.getAccounts()).find(
(account) => account.address === signerAddress,
);
if (!accountFromSigner) {
throw new Error("Failed to retrieve account from signer");
}
const pubkey = encodeSecp256k1Pubkey(accountFromSigner.pubkey);
const { sequence } = await this.getSequence(signerAddress);
const { gasInfo } = await this.forceGetQueryClient().tx.simulate(anyMsgs, memo, pubkey, sequence);
assertDefined(gasInfo);
return Uint53.fromString(gasInfo.gasUsed.toString()).toNumber();
}
/** Uploads code and returns a receipt, including the code ID */
public async upload(
senderAddress: string,
wasmCode: Uint8Array,
fee: StdFee,
fee: StdFee | "auto" | number,
memo = "",
): Promise<UploadResult> {
const compressed = pako.gzip(wasmCode, { level: 9 });
@ -234,7 +258,7 @@ export class SigningCosmWasmClient extends CosmWasmClient {
codeId: number,
msg: Record<string, unknown>,
label: string,
fee: StdFee,
fee: StdFee | "auto" | number,
options: InstantiateOptions = {},
): Promise<InstantiateResult> {
const instantiateContractMsg: MsgInstantiateContractEncodeObject = {
@ -265,7 +289,7 @@ export class SigningCosmWasmClient extends CosmWasmClient {
senderAddress: string,
contractAddress: string,
newAdmin: string,
fee: StdFee,
fee: StdFee | "auto" | number,
memo = "",
): Promise<ChangeAdminResult> {
const updateAdminMsg: MsgUpdateAdminEncodeObject = {
@ -289,7 +313,7 @@ export class SigningCosmWasmClient extends CosmWasmClient {
public async clearAdmin(
senderAddress: string,
contractAddress: string,
fee: StdFee,
fee: StdFee | "auto" | number,
memo = "",
): Promise<ChangeAdminResult> {
const clearAdminMsg: MsgClearAdminEncodeObject = {
@ -314,7 +338,7 @@ export class SigningCosmWasmClient extends CosmWasmClient {
contractAddress: string,
codeId: number,
migrateMsg: Record<string, unknown>,
fee: StdFee,
fee: StdFee | "auto" | number,
memo = "",
): Promise<MigrateResult> {
const migrateContractMsg: MsgMigrateContractEncodeObject = {
@ -340,7 +364,7 @@ export class SigningCosmWasmClient extends CosmWasmClient {
senderAddress: string,
contractAddress: string,
msg: Record<string, unknown>,
fee: StdFee,
fee: StdFee | "auto" | number,
memo = "",
funds?: readonly Coin[],
): Promise<ExecuteResult> {
@ -367,7 +391,7 @@ export class SigningCosmWasmClient extends CosmWasmClient {
senderAddress: string,
recipientAddress: string,
amount: readonly Coin[],
fee: StdFee,
fee: StdFee | "auto" | number,
memo = "",
): Promise<BroadcastTxResponse> {
const sendMsg: MsgSendEncodeObject = {
@ -385,7 +409,7 @@ export class SigningCosmWasmClient extends CosmWasmClient {
delegatorAddress: string,
validatorAddress: string,
amount: Coin,
fee: StdFee,
fee: StdFee | "auto" | number,
memo = "",
): Promise<BroadcastTxResponse> {
const delegateMsg: MsgDelegateEncodeObject = {
@ -399,7 +423,7 @@ export class SigningCosmWasmClient extends CosmWasmClient {
delegatorAddress: string,
validatorAddress: string,
amount: Coin,
fee: StdFee,
fee: StdFee | "auto" | number,
memo = "",
): Promise<BroadcastTxResponse> {
const undelegateMsg: MsgUndelegateEncodeObject = {
@ -412,7 +436,7 @@ export class SigningCosmWasmClient extends CosmWasmClient {
public async withdrawRewards(
delegatorAddress: string,
validatorAddress: string,
fee: StdFee,
fee: StdFee | "auto" | number,
memo = "",
): Promise<BroadcastTxResponse> {
const withdrawDelegatorRewardMsg: MsgWithdrawDelegatorRewardEncodeObject = {
@ -433,10 +457,19 @@ export class SigningCosmWasmClient extends CosmWasmClient {
public async signAndBroadcast(
signerAddress: string,
messages: readonly EncodeObject[],
fee: StdFee,
fee: StdFee | "auto" | number,
memo = "",
): Promise<BroadcastTxResponse> {
const txRaw = await this.sign(signerAddress, messages, fee, memo);
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 muliplier = typeof fee === "number" ? fee : 1.3;
usedFee = calculateFee(Math.round(gasEstimation * muliplier), this.gasPrice);
} else {
usedFee = fee;
}
const txRaw = await this.sign(signerAddress, messages, usedFee, memo);
const txBytes = TxRaw.encode(txRaw).finish();
return this.broadcastTx(txBytes, this.broadcastTimeoutMs, this.broadcastPollIntervalMs);
}

View File

@ -117,6 +117,13 @@ export class Registry {
return type;
}
/**
* Takes a typeUrl/value pair and encodes the value to protobuf if
* the given type was previously registered.
*
* If the value has to be wrapped in an Any, this needs to be done
* manually after this call. Or use `encodeAsAny` instead.
*/
public encode(encodeObject: EncodeObject): Uint8Array {
const { value, typeUrl } = encodeObject;
if (isTxBodyEncodeObject(encodeObject)) {
@ -127,14 +134,20 @@ export class Registry {
return type.encode(instance).finish();
}
public encodeTxBody(txBodyFields: TxBodyValue): Uint8Array {
const wrappedMessages = txBodyFields.messages.map((message) => {
const messageBytes = this.encode(message);
return Any.fromPartial({
typeUrl: message.typeUrl,
value: messageBytes,
});
/**
* Takes a typeUrl/value pair and encodes the value to an Any if
* the given type was previously registered.
*/
public encodeAsAny(encodeObject: EncodeObject): Any {
const binaryValue = this.encode(encodeObject);
return Any.fromPartial({
typeUrl: encodeObject.typeUrl,
value: binaryValue,
});
}
public encodeTxBody(txBodyFields: TxBodyValue): Uint8Array {
const wrappedMessages = txBodyFields.messages.map((message) => this.encodeAsAny(message));
const txBody = TxBody.fromPartial({
...txBodyFields,
messages: wrappedMessages,

View File

@ -78,7 +78,9 @@ export {
setupGovExtension,
setupIbcExtension,
setupStakingExtension,
setupTxExtension,
StakingExtension,
TxExtension,
} from "./queries";
export {
SearchByHeightQuery,

View File

@ -10,4 +10,5 @@ export { DistributionExtension, setupDistributionExtension } from "./distributio
export { setupGovExtension, GovExtension, GovProposalId, GovParamsType } from "./gov";
export { IbcExtension, setupIbcExtension } from "./ibc";
export { setupStakingExtension, StakingExtension } from "./staking";
export { setupTxExtension, TxExtension } from "./tx";
export { createPagination, createProtobufRpcClient, ProtobufRpcClient } from "./utils";

View File

@ -0,0 +1,104 @@
import { coin, coins, DirectSecp256k1HdWallet, Registry } from "@cosmjs/proto-signing";
import { Tendermint34Client } from "@cosmjs/tendermint-rpc";
import { assertDefined, sleep } from "@cosmjs/utils";
import { MsgDelegate } from "cosmjs-types/cosmos/staking/v1beta1/tx";
import Long from "long";
import { defaultRegistryTypes, SigningStargateClient } from "../signingstargateclient";
import { assertIsBroadcastTxSuccess, StargateClient } from "../stargateclient";
import {
defaultSigningClientOptions,
faucet,
makeRandomAddress,
pendingWithoutSimapp,
simapp,
simappEnabled,
validator,
} from "../testutils.spec";
import { QueryClient } from "./queryclient";
import { setupTxExtension, TxExtension } from "./tx";
import { longify } from "./utils";
async function makeClientWithTx(rpcUrl: string): Promise<[QueryClient & TxExtension, Tendermint34Client]> {
const tmClient = await Tendermint34Client.connect(rpcUrl);
return [QueryClient.withExtensions(tmClient, setupTxExtension), tmClient];
}
describe("TxExtension", () => {
const defaultFee = {
amount: coins(25000, "ucosm"),
gas: "1500000", // 1.5 million
};
let txHash: string | undefined;
let memo: string | undefined;
beforeAll(async () => {
if (simappEnabled()) {
const wallet = await DirectSecp256k1HdWallet.fromMnemonic(faucet.mnemonic);
const client = await SigningStargateClient.connectWithSigner(
simapp.tendermintUrl,
wallet,
defaultSigningClientOptions,
);
{
const recipient = makeRandomAddress();
memo = `Test tx ${Date.now()}`;
const result = await client.sendTokens(
faucet.address0,
recipient,
coins(25000, "ucosm"),
defaultFee,
memo,
);
assertIsBroadcastTxSuccess(result);
txHash = result.transactionHash;
}
await sleep(75); // wait until transactions are indexed
}
});
describe("getTx", () => {
it("works", async () => {
pendingWithoutSimapp();
assertDefined(txHash);
assertDefined(memo);
const [client, tmClient] = await makeClientWithTx(simapp.tendermintUrl);
const response = await client.tx.getTx(txHash);
expect(response.tx?.body?.memo).toEqual(memo);
tmClient.disconnect();
});
});
describe("simulate", () => {
it("works", async () => {
pendingWithoutSimapp();
assertDefined(txHash);
assertDefined(memo);
const [client, tmClient] = await makeClientWithTx(simapp.tendermintUrl);
const sequenceClient = await StargateClient.connect(simapp.tendermintUrl);
const registry = new Registry(defaultRegistryTypes);
const msg: MsgDelegate = {
delegatorAddress: faucet.address0,
validatorAddress: validator.validatorAddress,
amount: coin(25000, "ustake"),
};
const msgAny = registry.encodeAsAny({
typeUrl: "/cosmos.staking.v1beta1.MsgDelegate",
value: msg,
});
const { sequence } = await sequenceClient.getSequence(faucet.address0);
const response = await client.tx.simulate([msgAny], "foo", faucet.pubkey0, sequence);
expect(response.gasInfo?.gasUsed.toNumber()).toBeGreaterThanOrEqual(101_000);
expect(response.gasInfo?.gasUsed.toNumber()).toBeLessThanOrEqual(150_000);
expect(response.gasInfo?.gasWanted).toEqual(longify(Long.UZERO));
tmClient.disconnect();
});
});
});

View File

@ -0,0 +1,81 @@
import { Pubkey } from "@cosmjs/amino";
import { encodePubkey } from "@cosmjs/proto-signing";
import { SignMode } from "cosmjs-types/cosmos/tx/signing/v1beta1/signing";
import {
GetTxRequest,
GetTxResponse,
ServiceClientImpl,
SimulateRequest,
SimulateResponse,
} from "cosmjs-types/cosmos/tx/v1beta1/service";
import { AuthInfo, Fee, Tx, TxBody } from "cosmjs-types/cosmos/tx/v1beta1/tx";
import { Any } from "cosmjs-types/google/protobuf/any";
import Long from "long";
import { QueryClient } from "./queryclient";
import { createProtobufRpcClient } from "./utils";
export interface TxExtension {
readonly tx: {
getTx: (txId: string) => Promise<GetTxResponse>;
simulate: (
messages: readonly Any[],
memo: string | undefined,
signer: Pubkey,
sequence: number,
) => Promise<SimulateResponse>;
// Add here with tests:
// - broadcastTx
// - getTxsEvent
};
}
export function setupTxExtension(base: QueryClient): TxExtension {
// Use this service to get easy typed access to query methods
// This cannot be used for proof verification
const rpc = createProtobufRpcClient(base);
const queryService = new ServiceClientImpl(rpc);
return {
tx: {
getTx: async (txId: string) => {
const request: GetTxRequest = {
hash: txId,
};
const response = await queryService.GetTx(request);
return response;
},
simulate: async (
messages: readonly Any[],
memo: string | undefined,
signer: Pubkey,
sequence: number,
) => {
const request = SimulateRequest.fromPartial({
tx: Tx.fromPartial({
authInfo: AuthInfo.fromPartial({
fee: Fee.fromPartial({}),
signerInfos: [
{
publicKey: encodePubkey(signer),
sequence: Long.fromNumber(sequence, true),
modeInfo: { single: { mode: SignMode.SIGN_MODE_UNSPECIFIED } },
},
],
}),
body: TxBody.fromPartial({
messages: Array.from(messages),
memo: memo,
}),
signatures: [new Uint8Array()],
}),
// Sending serialized `txBytes` is the future. But
// this is not available in Comsos SDK 0.42.
txBytes: undefined,
});
const response = await queryService.Simulate(request);
return response;
},
},
};
}

View File

@ -16,6 +16,7 @@ import { MsgDelegateEncodeObject, MsgSendEncodeObject } from "./encodeobjects";
import { PrivateSigningStargateClient, SigningStargateClient } from "./signingstargateclient";
import { assertIsBroadcastTxSuccess, isBroadcastTxFailure } from "./stargateclient";
import {
defaultGasPrice,
defaultSendFee,
defaultSigningClientOptions,
faucet,
@ -42,6 +43,34 @@ describe("SigningStargateClient", () => {
});
});
describe("simulate", () => {
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 memo = "Use your power wisely";
const gasUsed = await client.simulate(faucet.address0, [msgAny], memo);
expect(gasUsed).toBeGreaterThanOrEqual(101_000);
expect(gasUsed).toBeLessThanOrEqual(150_000);
client.disconnect();
});
});
describe("sendTokens", () => {
it("works with direct signer", async () => {
pendingWithoutSimapp();
@ -245,6 +274,27 @@ describe("SigningStargateClient", () => {
assertIsBroadcastTxSuccess(result);
});
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 result = await client.signAndBroadcast(faucet.address0, [msgAny], "auto");
assertIsBroadcastTxSuccess(result);
});
it("works with a modifying signer", async () => {
pendingWithoutSimapp();
const wallet = await ModifyingDirectSecp256k1HdWallet.fromMnemonic(faucet.mnemonic);

View File

@ -1,6 +1,6 @@
import { encodeSecp256k1Pubkey, makeSignDoc as makeSignDocAmino, StdFee } from "@cosmjs/amino";
import { fromBase64 } from "@cosmjs/encoding";
import { Int53 } from "@cosmjs/math";
import { Int53, Uint53 } from "@cosmjs/math";
import {
EncodeObject,
encodePubkey,
@ -13,7 +13,7 @@ import {
TxBodyEncodeObject,
} from "@cosmjs/proto-signing";
import { Tendermint34Client } from "@cosmjs/tendermint-rpc";
import { assert } from "@cosmjs/utils";
import { assert, assertDefined } from "@cosmjs/utils";
import { MsgMultiSend } from "cosmjs-types/cosmos/bank/v1beta1/tx";
import { Coin } from "cosmjs-types/cosmos/base/v1beta1/coin";
import {
@ -68,6 +68,7 @@ import {
MsgUndelegateEncodeObject,
MsgWithdrawDelegatorRewardEncodeObject,
} from "./encodeobjects";
import { calculateFee, GasPrice } from "./fee";
import { BroadcastTxResponse, StargateClient } from "./stargateclient";
export const defaultRegistryTypes: ReadonlyArray<[string, GeneratedType]> = [
@ -131,6 +132,7 @@ export interface SigningStargateClientOptions {
readonly prefix?: string;
readonly broadcastTimeoutMs?: number;
readonly broadcastPollIntervalMs?: number;
readonly gasPrice?: GasPrice;
}
export class SigningStargateClient extends StargateClient {
@ -140,6 +142,7 @@ export class SigningStargateClient extends StargateClient {
private readonly signer: OfflineSigner;
private readonly aminoTypes: AminoTypes;
private readonly gasPrice: GasPrice | undefined;
public static async connectWithSigner(
endpoint: string,
@ -179,13 +182,33 @@ export class SigningStargateClient extends StargateClient {
this.signer = signer;
this.broadcastTimeoutMs = options.broadcastTimeoutMs;
this.broadcastPollIntervalMs = options.broadcastPollIntervalMs;
this.gasPrice = options.gasPrice;
}
public async simulate(
signerAddress: string,
messages: readonly EncodeObject[],
memo: string | undefined,
): Promise<number> {
const anyMsgs = messages.map((m) => this.registry.encodeAsAny(m));
const accountFromSigner = (await this.signer.getAccounts()).find(
(account) => account.address === signerAddress,
);
if (!accountFromSigner) {
throw new Error("Failed to retrieve account from signer");
}
const pubkey = encodeSecp256k1Pubkey(accountFromSigner.pubkey);
const { sequence } = await this.getSequence(signerAddress);
const { gasInfo } = await this.forceGetQueryClient().tx.simulate(anyMsgs, memo, pubkey, sequence);
assertDefined(gasInfo);
return Uint53.fromString(gasInfo.gasUsed.toString()).toNumber();
}
public async sendTokens(
senderAddress: string,
recipientAddress: string,
amount: readonly Coin[],
fee: StdFee,
fee: StdFee | "auto" | number,
memo = "",
): Promise<BroadcastTxResponse> {
const sendMsg: MsgSendEncodeObject = {
@ -203,7 +226,7 @@ export class SigningStargateClient extends StargateClient {
delegatorAddress: string,
validatorAddress: string,
amount: Coin,
fee: StdFee,
fee: StdFee | "auto" | number,
memo = "",
): Promise<BroadcastTxResponse> {
const delegateMsg: MsgDelegateEncodeObject = {
@ -221,7 +244,7 @@ export class SigningStargateClient extends StargateClient {
delegatorAddress: string,
validatorAddress: string,
amount: Coin,
fee: StdFee,
fee: StdFee | "auto" | number,
memo = "",
): Promise<BroadcastTxResponse> {
const undelegateMsg: MsgUndelegateEncodeObject = {
@ -238,7 +261,7 @@ export class SigningStargateClient extends StargateClient {
public async withdrawRewards(
delegatorAddress: string,
validatorAddress: string,
fee: StdFee,
fee: StdFee | "auto" | number,
memo = "",
): Promise<BroadcastTxResponse> {
const withdrawMsg: MsgWithdrawDelegatorRewardEncodeObject = {
@ -260,7 +283,7 @@ export class SigningStargateClient extends StargateClient {
timeoutHeight: Height | undefined,
/** timeout in seconds */
timeoutTimestamp: number | undefined,
fee: StdFee,
fee: StdFee | "auto" | number,
memo = "",
): Promise<BroadcastTxResponse> {
const timeoutTimestampNanoseconds = timeoutTimestamp
@ -284,10 +307,19 @@ export class SigningStargateClient extends StargateClient {
public async signAndBroadcast(
signerAddress: string,
messages: readonly EncodeObject[],
fee: StdFee,
fee: StdFee | "auto" | number,
memo = "",
): Promise<BroadcastTxResponse> {
const txRaw = await this.sign(signerAddress, messages, fee, memo);
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 muliplier = typeof fee === "number" ? fee : 1.3;
usedFee = calculateFee(Math.round(gasEstimation * muliplier), this.gasPrice);
} else {
usedFee = fee;
}
const txRaw = await this.sign(signerAddress, messages, usedFee, memo);
const txBytes = TxRaw.encode(txRaw).finish();
return this.broadcastTx(txBytes, this.broadcastTimeoutMs, this.broadcastPollIntervalMs);
}

View File

@ -14,7 +14,9 @@ import {
setupAuthExtension,
setupBankExtension,
setupStakingExtension,
setupTxExtension,
StakingExtension,
TxExtension,
} from "./queries";
import {
isSearchByHeightQuery,
@ -146,7 +148,9 @@ export interface PrivateStargateClient {
export class StargateClient {
private readonly tmClient: Tendermint34Client | undefined;
private readonly queryClient: (QueryClient & AuthExtension & BankExtension & StakingExtension) | undefined;
private readonly queryClient:
| (QueryClient & AuthExtension & BankExtension & StakingExtension & TxExtension)
| undefined;
private chainId: string | undefined;
public static async connect(endpoint: string): Promise<StargateClient> {
@ -162,6 +166,7 @@ export class StargateClient {
setupAuthExtension,
setupBankExtension,
setupStakingExtension,
setupTxExtension,
);
}
}
@ -179,11 +184,17 @@ export class StargateClient {
return this.tmClient;
}
protected getQueryClient(): (QueryClient & AuthExtension & BankExtension & StakingExtension) | undefined {
protected getQueryClient():
| (QueryClient & AuthExtension & BankExtension & StakingExtension & TxExtension)
| undefined {
return this.queryClient;
}
protected forceGetQueryClient(): QueryClient & AuthExtension & BankExtension & StakingExtension {
protected forceGetQueryClient(): QueryClient &
AuthExtension &
BankExtension &
StakingExtension &
TxExtension {
if (!this.queryClient) {
throw new Error("Query client not available. You cannot use online functionality in offline mode.");
}