Merge pull request #931 from cosmos/add-simulate
Add simulation support
This commit is contained in:
commit
0d9108e3a1
123
packages/cli/examples/simulate.ts
Normal file
123
packages/cli/examples/simulate.ts
Normal 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();
|
||||
@ -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",
|
||||
);
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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.");
|
||||
}
|
||||
|
||||
@ -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, {
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -78,7 +78,9 @@ export {
|
||||
setupGovExtension,
|
||||
setupIbcExtension,
|
||||
setupStakingExtension,
|
||||
setupTxExtension,
|
||||
StakingExtension,
|
||||
TxExtension,
|
||||
} from "./queries";
|
||||
export {
|
||||
SearchByHeightQuery,
|
||||
|
||||
@ -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";
|
||||
|
||||
104
packages/stargate/src/queries/tx.spec.ts
Normal file
104
packages/stargate/src/queries/tx.spec.ts
Normal 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();
|
||||
});
|
||||
});
|
||||
});
|
||||
81
packages/stargate/src/queries/tx.ts
Normal file
81
packages/stargate/src/queries/tx.ts
Normal 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;
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
@ -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);
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
@ -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.");
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user