diff --git a/packages/stargate/src/client.ts b/packages/stargate/src/client.ts new file mode 100644 index 0000000000..0499bfd40c --- /dev/null +++ b/packages/stargate/src/client.ts @@ -0,0 +1,688 @@ +import { encodeSecp256k1Pubkey, makeSignDoc as makeSignDocAmino, StdFee } from "@cosmjs/amino"; +import { fromBase64, toHex } from "@cosmjs/encoding"; +import { Int53, Uint53 } from "@cosmjs/math"; +import { + EncodeObject, + encodePubkey, + isOfflineDirectSigner, + makeAuthInfoBytes, + makeSignDoc, + OfflineSigner, + Registry, + TxBodyEncodeObject, +} from "@cosmjs/proto-signing"; +import { CometClient, connectComet, HttpEndpoint, toRfc3339WithNanoseconds } from "@cosmjs/tendermint-rpc"; +import { assert, assertDefined, sleep } from "@cosmjs/utils"; +import { MsgData, TxMsgData } from "cosmjs-types/cosmos/base/abci/v1beta1/abci"; +import { SignMode } from "cosmjs-types/cosmos/tx/signing/v1beta1/signing"; +import { TxRaw } from "cosmjs-types/cosmos/tx/v1beta1/tx"; + +import { Account, accountFromAny, AccountParser } from "./accounts"; +import { AminoTypes } from "./aminotypes"; +import { Event, fromTendermintEvent } from "./events"; +import { calculateFee, GasPrice } from "./fee"; +import { AuthExtension, setupAuthExtension, setupTxExtension, TxExtension } from "./modules"; +import { QueryClient } from "./queryclient"; +import { SearchTxQuery } from "./search"; + +export interface BlockHeader { + readonly version: { + readonly block: string; + readonly app: string; + }; + readonly height: number; + readonly chainId: string; + /** An RFC 3339 time string like e.g. '2020-02-15T10:39:10.4696305Z' */ + readonly time: string; +} + +export interface Block { + /** The ID is a hash of the block header (uppercase hex) */ + readonly id: string; + readonly header: BlockHeader; + /** Array of raw transactions */ + readonly txs: readonly Uint8Array[]; +} + +export interface SequenceResponse { + readonly accountNumber: number; + readonly sequence: number; +} + +/** + * The response after successfully broadcasting a transaction. + * Success or failure refer to the execution result. + */ +export interface DeliverTxResponse { + readonly height: number; + /** The position of the transaction within the block. This is a 0-based index. */ + readonly txIndex: number; + /** Error code. The transaction suceeded iff code is 0. */ + readonly code: number; + readonly transactionHash: string; + readonly events: readonly Event[]; + /** + * A string-based log document. + * + * This currently seems to merge attributes of multiple events into one event per type + * (https://github.com/tendermint/tendermint/issues/9595). You might want to use the `events` + * field instead. + */ + readonly rawLog?: string; + /** @deprecated Use `msgResponses` instead. */ + readonly data?: readonly MsgData[]; + /** + * The message responses of the [TxMsgData](https://github.com/cosmos/cosmos-sdk/blob/v0.46.3/proto/cosmos/base/abci/v1beta1/abci.proto#L128-L140) + * as `Any`s. + * This field is an empty list for chains running Cosmos SDK < 0.46. + */ + readonly msgResponses: Array<{ readonly typeUrl: string; readonly value: Uint8Array }>; + readonly gasUsed: number; + readonly gasWanted: number; +} + +export function isDeliverTxFailure(result: DeliverTxResponse): boolean { + return !!result.code; +} + +export function isDeliverTxSuccess(result: DeliverTxResponse): boolean { + return !isDeliverTxFailure(result); +} + +/** + * Ensures the given result is a success. Throws a detailed error message otherwise. + */ +export function assertIsDeliverTxSuccess(result: DeliverTxResponse): void { + if (isDeliverTxFailure(result)) { + throw new Error( + `Error when broadcasting tx ${result.transactionHash} at height ${result.height}. Code: ${result.code}; Raw log: ${result.rawLog}`, + ); + } +} + +/** + * Ensures the given result is a failure. Throws a detailed error message otherwise. + */ +export function assertIsDeliverTxFailure(result: DeliverTxResponse): void { + if (isDeliverTxSuccess(result)) { + throw new Error( + `Transaction ${result.transactionHash} did not fail at height ${result.height}. Code: ${result.code}; Raw log: ${result.rawLog}`, + ); + } +} + +/** A transaction that is indexed as part of the transaction history */ +export interface IndexedTx { + readonly height: number; + /** The position of the transaction within the block. This is a 0-based index. */ + readonly txIndex: number; + /** Transaction hash (might be used as transaction ID). Guaranteed to be non-empty upper-case hex */ + readonly hash: string; + /** Transaction execution error code. 0 on success. */ + readonly code: number; + readonly events: readonly Event[]; + /** + * A string-based log document. + * + * This currently seems to merge attributes of multiple events into one event per type + * (https://github.com/tendermint/tendermint/issues/9595). You might want to use the `events` + * field instead. + */ + readonly rawLog: string; + /** + * Raw transaction bytes stored in Tendermint. + * + * If you hash this, you get the transaction hash (= transaction ID): + * + * ```js + * import { sha256 } from "@cosmjs/crypto"; + * import { toHex } from "@cosmjs/encoding"; + * + * const transactionId = toHex(sha256(indexTx.tx)).toUpperCase(); + * ``` + * + * Use `decodeTxRaw` from @cosmjs/proto-signing to decode this. + */ + readonly tx: Uint8Array; + /** + * The message responses of the [TxMsgData](https://github.com/cosmos/cosmos-sdk/blob/v0.46.3/proto/cosmos/base/abci/v1beta1/abci.proto#L128-L140) + * as `Any`s. + * This field is an empty list for chains running Cosmos SDK < 0.46. + */ + readonly msgResponses: Array<{ readonly typeUrl: string; readonly value: Uint8Array }>; + readonly gasUsed: number; + readonly gasWanted: number; +} + +/** + * An error when broadcasting the transaction. This contains the CheckTx errors + * from the blockchain. Once a transaction is included in a block no BroadcastTxError + * is thrown, even if the execution fails (DeliverTx errors). + */ +export class BroadcastTxError extends Error { + public readonly code: number; + public readonly codespace: string; + public readonly log: string | undefined; + + public constructor(code: number, codespace: string, log: string | undefined) { + super(`Broadcasting transaction failed with code ${code} (codespace: ${codespace}). Log: ${log}`); + this.code = code; + this.codespace = codespace; + this.log = log; + } +} + +export class TimeoutError extends Error { + public readonly txId: string; + + public constructor(message: string, txId: string) { + super(message); + this.txId = txId; + } +} + +/** + * Signing information for a single signer that is not included in the transaction. + * + * @see https://github.com/cosmos/cosmos-sdk/blob/v0.42.2/x/auth/signing/sign_mode_handler.go#L23-L37 + */ +export interface SignerData { + readonly accountNumber: number; + readonly sequence: number; + readonly chainId: string; +} + +/** Use for testing only */ +export interface PrivateClient { + readonly registry: Registry; +} + +export interface ClientOptions { + readonly accountParser?: AccountParser; + readonly registry?: Registry; + readonly aminoTypes?: AminoTypes; + readonly broadcastTimeoutMs?: number; + readonly broadcastPollIntervalMs?: number; + readonly gasPrice?: GasPrice; +} + +export class Client { + public readonly registry: Registry; + public readonly broadcastTimeoutMs: number | undefined; + public readonly broadcastPollIntervalMs: number | undefined; + + private readonly cometClient: CometClient | undefined; + private readonly queryClient: (QueryClient & AuthExtension & TxExtension) | undefined; + /** Chain ID cache */ + private chainId: string | undefined; + private readonly accountParser: AccountParser; + private readonly signer: OfflineSigner | undefined; + private readonly aminoTypes: AminoTypes; + private readonly gasPrice: GasPrice | undefined; + + /** + * Creates an instance by connecting to the given Tendermint RPC endpoint. + * + * This uses auto-detection to decide between a Tendermint 0.37 and 0.34 client. + * To set the Tendermint client explicitly, use `createWithSigner`. + */ + public static async connectWithSigner( + endpoint: string | HttpEndpoint, + signer: OfflineSigner, + options: ClientOptions = {}, + ): Promise { + const cometClient = await connectComet(endpoint); + return Client.createWithSigner(cometClient, signer, options); + } + + /** + * Creates an instance from a manually created Comet client. + * Use this to use `Tendermint37Client` instead of `Tendermint34Client`. + */ + public static async createWithSigner( + cometClient: CometClient, + signer: OfflineSigner, + options: ClientOptions = {}, + ): Promise { + return new Client(cometClient, signer, options); + } + + /** + * Creates an instance without signer from a manually created Comet client. + */ + public static async create(cometClient: CometClient, options: ClientOptions = {}): Promise { + return new Client(cometClient, undefined, options); + } + + /** + * Creates a client in offline mode. + * + * This should only be used in niche cases where you know exactly what you're doing, + * e.g. when building an offline signing application. + * + * When you try to use online functionality with such a signer, an + * exception will be raised. + */ + public static async offline(signer: OfflineSigner, options: ClientOptions = {}): Promise { + return new Client(undefined, signer, options); + } + + public constructor( + cometClient: CometClient | undefined, + signer: OfflineSigner | undefined, + options: ClientOptions, + ) { + if (cometClient) { + this.cometClient = cometClient; + this.queryClient = QueryClient.withExtensions(cometClient, setupAuthExtension, setupTxExtension); + } + const { + registry = new Registry(), + aminoTypes = new AminoTypes({}), + accountParser = accountFromAny, + } = options; + this.accountParser = accountParser; + this.registry = registry; + this.aminoTypes = aminoTypes; + this.signer = signer; + this.broadcastTimeoutMs = options.broadcastTimeoutMs; + this.broadcastPollIntervalMs = options.broadcastPollIntervalMs; + this.gasPrice = options.gasPrice; + } + + public getCometClient(): CometClient | undefined { + return this.cometClient; + } + + public forceGetCometClient(): CometClient { + if (!this.cometClient) { + throw new Error("Comet client not available. You cannot use online functionality in offline mode."); + } + return this.cometClient; + } + + protected getQueryClient(): (QueryClient & AuthExtension & TxExtension) | undefined { + return this.queryClient; + } + + protected forceGetQueryClient(): QueryClient & AuthExtension & TxExtension { + if (!this.queryClient) { + throw new Error("Query client not available. You cannot use online functionality in offline mode."); + } + return this.queryClient; + } + + public async getAccount(searchAddress: string): Promise { + try { + const account = await this.forceGetQueryClient().auth.account(searchAddress); + return account ? this.accountParser(account) : null; + } catch (error: any) { + if (/rpc error: code = NotFound/i.test(error.toString())) { + return null; + } + throw error; + } + } + + public async getSequence(address: string): Promise { + const account = await this.getAccount(address); + if (!account) { + throw new Error( + `Account '${address}' does not exist on chain. Send some tokens there before trying to query sequence.`, + ); + } + return { + accountNumber: account.accountNumber, + sequence: account.sequence, + }; + } + + public async getBlock(height?: number): Promise { + const response = await this.forceGetCometClient().block(height); + return { + id: toHex(response.blockId.hash).toUpperCase(), + header: { + version: { + block: new Uint53(response.block.header.version.block).toString(), + app: new Uint53(response.block.header.version.app).toString(), + }, + height: response.block.header.height, + chainId: response.block.header.chainId, + time: toRfc3339WithNanoseconds(response.block.header.time), + }, + txs: response.block.txs, + }; + } + + public async simulate( + signerAddress: string, + messages: readonly EncodeObject[], + memo: string | undefined, + ): Promise { + const anyMsgs = messages.map((m) => this.registry.encodeAsAny(m)); + assert( + this.signer, + "Simulation requires a signer. FIXME: create workaround for this limitation (https://github.com/cosmos/cosmjs/issues/1213).", + ); + 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 getChainId(): Promise { + if (!this.chainId) { + const response = await this.forceGetCometClient().status(); + const chainId = response.nodeInfo.network; + if (!chainId) throw new Error("Chain ID must not be empty"); + this.chainId = chainId; + } + + return this.chainId; + } + + public async signAndBroadcast( + signerAddress: string, + messages: readonly EncodeObject[], + fee: StdFee | "auto" | number, + memo = "", + ): Promise { + let usedFee: StdFee; + if (fee == "auto" || typeof fee === "number") { + assertDefined(this.gasPrice, "Gas price must be set in the client options when auto gas is used."); + const gasEstimation = await this.simulate(signerAddress, messages, memo); + // Starting with Cosmos SDK 0.47, we see many cases in which 1.3 is not enough anymore + // E.g. https://github.com/cosmos/cosmos-sdk/issues/16020 + const multiplier = typeof fee === "number" ? fee : 1.4; + 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.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 { + 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. + * + * The sign mode (SIGN_MODE_DIRECT or SIGN_MODE_LEGACY_AMINO_JSON) is determined by this client's signer. + * + * You can pass signer data (account number, sequence and chain ID) explicitly instead of querying them + * from the chain. This is needed when signing for a multisig account, but it also allows for offline signing + * (See the SigningStargateClient.offline constructor). + */ + public async sign( + signerAddress: string, + messages: readonly EncodeObject[], + fee: StdFee, + memo: string, + explicitSignerData?: SignerData, + ): Promise { + assert(this.signer, "No signer set for this client instance"); + let signerData: SignerData; + if (explicitSignerData) { + signerData = explicitSignerData; + } else { + const { accountNumber, sequence } = await this.getSequence(signerAddress); + const chainId = await this.getChainId(); + signerData = { + accountNumber: accountNumber, + sequence: sequence, + chainId: chainId, + }; + } + return isOfflineDirectSigner(this.signer) + ? this.signDirect(signerAddress, messages, fee, memo, signerData) + : this.signAmino(signerAddress, messages, fee, memo, signerData); + } + + private async signAmino( + signerAddress: string, + messages: readonly EncodeObject[], + fee: StdFee, + memo: string, + { accountNumber, sequence, chainId }: SignerData, + ): Promise { + assert(this.signer, "No signer set for this client instance"); + assert(!isOfflineDirectSigner(this.signer)); + const accountFromSigner = (await this.signer.getAccounts()).find( + (account) => account.address === signerAddress, + ); + if (!accountFromSigner) { + throw new Error("Failed to retrieve account from signer"); + } + const pubkey = encodePubkey(encodeSecp256k1Pubkey(accountFromSigner.pubkey)); + const signMode = SignMode.SIGN_MODE_LEGACY_AMINO_JSON; + const msgs = messages.map((msg) => this.aminoTypes.toAmino(msg)); + const signDoc = makeSignDocAmino(msgs, fee, chainId, memo, accountNumber, sequence); + const { signature, signed } = await this.signer.signAmino(signerAddress, signDoc); + const signedTxBody = { + messages: signed.msgs.map((msg) => this.aminoTypes.fromAmino(msg)), + memo: signed.memo, + }; + const signedTxBodyEncodeObject: TxBodyEncodeObject = { + typeUrl: "/cosmos.tx.v1beta1.TxBody", + value: signedTxBody, + }; + const signedTxBodyBytes = this.registry.encode(signedTxBodyEncodeObject); + const signedGasLimit = Int53.fromString(signed.fee.gas).toNumber(); + const signedSequence = Int53.fromString(signed.sequence).toNumber(); + const signedAuthInfoBytes = makeAuthInfoBytes( + [{ pubkey, sequence: signedSequence }], + signed.fee.amount, + signedGasLimit, + signed.fee.granter, + signed.fee.payer, + signMode, + ); + return TxRaw.fromPartial({ + bodyBytes: signedTxBodyBytes, + authInfoBytes: signedAuthInfoBytes, + signatures: [fromBase64(signature.signature)], + }); + } + + private async signDirect( + signerAddress: string, + messages: readonly EncodeObject[], + fee: StdFee, + memo: string, + { accountNumber, sequence, chainId }: SignerData, + ): Promise { + assert(this.signer, "No signer set for this client instance"); + assert(isOfflineDirectSigner(this.signer)); + const accountFromSigner = (await this.signer.getAccounts()).find( + (account) => account.address === signerAddress, + ); + if (!accountFromSigner) { + throw new Error("Failed to retrieve account from signer"); + } + const pubkey = encodePubkey(encodeSecp256k1Pubkey(accountFromSigner.pubkey)); + const txBodyEncodeObject: TxBodyEncodeObject = { + typeUrl: "/cosmos.tx.v1beta1.TxBody", + value: { + messages: messages, + memo: memo, + }, + }; + const txBodyBytes = this.registry.encode(txBodyEncodeObject); + const gasLimit = Int53.fromString(fee.gas).toNumber(); + const authInfoBytes = makeAuthInfoBytes( + [{ pubkey, sequence }], + fee.amount, + gasLimit, + fee.granter, + fee.payer, + ); + const signDoc = makeSignDoc(txBodyBytes, authInfoBytes, chainId, accountNumber); + const { signature, signed } = await this.signer.signDirect(signerAddress, signDoc); + return TxRaw.fromPartial({ + bodyBytes: signed.bodyBytes, + authInfoBytes: signed.authInfoBytes, + signatures: [fromBase64(signature.signature)], + }); + } + + public async getTx(id: string): Promise { + const results = await this.txsQuery(`tx.hash='${id}'`); + return results[0] ?? null; + } + + public async searchTx(query: SearchTxQuery): Promise { + let rawQuery: string; + if (typeof query === "string") { + rawQuery = query; + } else if (Array.isArray(query)) { + rawQuery = query.map((t) => `${t.key}='${t.value}'`).join(" AND "); + } else { + throw new Error("Got unsupported query type. See CosmJS 0.31 CHANGELOG for API breaking changes here."); + } + return this.txsQuery(rawQuery); + } + + public disconnect(): void { + if (this.cometClient) this.cometClient.disconnect(); + } + + /** + * Broadcasts a signed transaction to the network and monitors its inclusion in a block. + * + * 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 not included in a block before the provided timeout, this errors with a `TimeoutError`. + * + * If the transaction is included in a block, a `DeliverTxResponse` is returned. The caller then + * usually needs to check for execution success or failure. + */ + public async broadcastTx( + tx: Uint8Array, + timeoutMs = 60_000, + pollIntervalMs = 3_000, + ): Promise { + let timedOut = false; + const txPollTimeout = setTimeout(() => { + timedOut = true; + }, timeoutMs); + + const pollForTx = async (txId: string): Promise => { + if (timedOut) { + throw new TimeoutError( + `Transaction with ID ${txId} was submitted but was not yet found on the chain. You might want to check later. There was a wait of ${ + timeoutMs / 1000 + } seconds.`, + txId, + ); + } + await sleep(pollIntervalMs); + const result = await this.getTx(txId); + return result + ? { + code: result.code, + height: result.height, + txIndex: result.txIndex, + events: result.events, + rawLog: result.rawLog, + transactionHash: txId, + msgResponses: result.msgResponses, + gasUsed: result.gasUsed, + gasWanted: result.gasWanted, + } + : pollForTx(txId); + }; + + const transactionId = await this.broadcastTxSync(tx); + + return new Promise((resolve, reject) => + pollForTx(transactionId).then( + (value) => { + clearTimeout(txPollTimeout); + resolve(value); + }, + (error) => { + clearTimeout(txPollTimeout); + reject(error); + }, + ), + ); + } + + /** + * 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 { + const broadcasted = await this.forceGetCometClient().broadcastTxSync({ tx }); + + if (broadcasted.code) { + return Promise.reject( + new BroadcastTxError(broadcasted.code, broadcasted.codespace ?? "", broadcasted.log), + ); + } + + const transactionId = toHex(broadcasted.hash).toUpperCase(); + + return transactionId; + } + + public async txsQuery(query: string): Promise { + const results = await this.forceGetCometClient().txSearchAll({ query: query }); + return results.txs.map((tx): IndexedTx => { + const txMsgData = TxMsgData.decode(tx.result.data ?? new Uint8Array()); + return { + height: tx.height, + txIndex: tx.index, + hash: toHex(tx.hash).toUpperCase(), + code: tx.result.code, + events: tx.result.events.map(fromTendermintEvent), + rawLog: tx.result.log || "", + tx: tx.tx, + msgResponses: txMsgData.msgResponses, + gasUsed: tx.result.gasUsed, + gasWanted: tx.result.gasWanted, + }; + }); + } +} diff --git a/packages/stargate/src/events.ts b/packages/stargate/src/events.ts index 57f12aaa47..03f20361eb 100644 --- a/packages/stargate/src/events.ts +++ b/packages/stargate/src/events.ts @@ -1,5 +1,5 @@ import { fromUtf8 } from "@cosmjs/encoding"; -import { tendermint34, tendermint37 } from "@cosmjs/tendermint-rpc"; +import { comet38, tendermint34, tendermint37 } from "@cosmjs/tendermint-rpc"; /** * An event attribute. @@ -33,7 +33,7 @@ export interface Event { * Takes a Tendermint 0.34 or 0.37 event with binary encoded key and value * and converts it into an `Event` with string attributes. */ -export function fromTendermintEvent(event: tendermint34.Event | tendermint37.Event): Event { +export function fromTendermintEvent(event: tendermint34.Event | tendermint37.Event | comet38.Event): Event { return { type: event.type, attributes: event.attributes.map( diff --git a/packages/stargate/src/index.ts b/packages/stargate/src/index.ts index d009351df8..d44c4d036c 100644 --- a/packages/stargate/src/index.ts +++ b/packages/stargate/src/index.ts @@ -1,5 +1,20 @@ export { Account, accountFromAny, AccountParser } from "./accounts"; export { AminoConverter, AminoConverters, AminoTypes } from "./aminotypes"; +export { + assertIsDeliverTxFailure, + assertIsDeliverTxSuccess, + Block, + BlockHeader, + BroadcastTxError, + Client, + ClientOptions, + DeliverTxResponse, + IndexedTx, + isDeliverTxFailure, + isDeliverTxSuccess, + SequenceResponse, + TimeoutError, +} from "./client"; export { Attribute, Event, fromTendermintEvent } from "./events"; export { calculateFee, GasPrice } from "./fee"; export * as logs from "./logs"; @@ -121,21 +136,7 @@ export { SigningStargateClient, SigningStargateClientOptions, } from "./signingstargateclient"; -export { - assertIsDeliverTxFailure, - assertIsDeliverTxSuccess, - Block, - BlockHeader, - BroadcastTxError, - DeliverTxResponse, - IndexedTx, - isDeliverTxFailure, - isDeliverTxSuccess, - SequenceResponse, - StargateClient, - StargateClientOptions, - TimeoutError, -} from "./stargateclient"; +export { StargateClient, StargateClientOptions } from "./stargateclient"; export { StdFee } from "@cosmjs/amino"; export { Coin, coin, coins, makeCosmoshubPath, parseCoins } from "@cosmjs/proto-signing"; diff --git a/packages/stargate/src/modules/authz/queries.spec.ts b/packages/stargate/src/modules/authz/queries.spec.ts index 364552234b..9de0afdee8 100644 --- a/packages/stargate/src/modules/authz/queries.spec.ts +++ b/packages/stargate/src/modules/authz/queries.spec.ts @@ -4,9 +4,9 @@ import { Tendermint34Client } from "@cosmjs/tendermint-rpc"; import { assertDefined, sleep } from "@cosmjs/utils"; import { GenericAuthorization } from "cosmjs-types/cosmos/authz/v1beta1/authz"; +import { assertIsDeliverTxSuccess } from "../../client"; import { QueryClient } from "../../queryclient"; import { SigningStargateClient } from "../../signingstargateclient"; -import { assertIsDeliverTxSuccess } from "../../stargateclient"; import { defaultSigningClientOptions, faucet, diff --git a/packages/stargate/src/modules/distribution/queries.spec.ts b/packages/stargate/src/modules/distribution/queries.spec.ts index a059d07a6d..975693a883 100644 --- a/packages/stargate/src/modules/distribution/queries.spec.ts +++ b/packages/stargate/src/modules/distribution/queries.spec.ts @@ -4,9 +4,9 @@ import { Tendermint34Client } from "@cosmjs/tendermint-rpc"; import { sleep } from "@cosmjs/utils"; import { MsgDelegate } from "cosmjs-types/cosmos/staking/v1beta1/tx"; +import { assertIsDeliverTxSuccess } from "../../client"; import { QueryClient } from "../../queryclient"; import { SigningStargateClient } from "../../signingstargateclient"; -import { assertIsDeliverTxSuccess } from "../../stargateclient"; import { defaultSigningClientOptions, faucet, diff --git a/packages/stargate/src/modules/gov/messages.spec.ts b/packages/stargate/src/modules/gov/messages.spec.ts index 378147ea03..9e2b52c9df 100644 --- a/packages/stargate/src/modules/gov/messages.spec.ts +++ b/packages/stargate/src/modules/gov/messages.spec.ts @@ -4,9 +4,9 @@ import { assert, sleep } from "@cosmjs/utils"; import { TextProposal, VoteOption } from "cosmjs-types/cosmos/gov/v1beta1/gov"; import { Any } from "cosmjs-types/google/protobuf/any"; +import { assertIsDeliverTxSuccess } from "../../client"; import { longify } from "../../queryclient"; import { SigningStargateClient } from "../../signingstargateclient"; -import { assertIsDeliverTxSuccess } from "../../stargateclient"; import { defaultSigningClientOptions, faucet, diff --git a/packages/stargate/src/modules/gov/queries.spec.ts b/packages/stargate/src/modules/gov/queries.spec.ts index 80de9d8686..59669c097d 100644 --- a/packages/stargate/src/modules/gov/queries.spec.ts +++ b/packages/stargate/src/modules/gov/queries.spec.ts @@ -13,9 +13,9 @@ import { import { Any } from "cosmjs-types/google/protobuf/any"; import Long from "long"; +import { assertIsDeliverTxSuccess } from "../../client"; import { longify, QueryClient } from "../../queryclient"; import { SigningStargateClient } from "../../signingstargateclient"; -import { assertIsDeliverTxSuccess } from "../../stargateclient"; import { defaultSigningClientOptions, faucet, diff --git a/packages/stargate/src/modules/staking/messages.spec.ts b/packages/stargate/src/modules/staking/messages.spec.ts index 5b8f5208da..11f9b3a295 100644 --- a/packages/stargate/src/modules/staking/messages.spec.ts +++ b/packages/stargate/src/modules/staking/messages.spec.ts @@ -3,9 +3,9 @@ import { Random } from "@cosmjs/crypto"; import { fromBech32, toBase64, toBech32 } from "@cosmjs/encoding"; import { DirectSecp256k1HdWallet, encodePubkey } from "@cosmjs/proto-signing"; +import { assertIsDeliverTxSuccess } from "../../client"; import { calculateFee } from "../../fee"; import { SigningStargateClient } from "../../signingstargateclient"; -import { assertIsDeliverTxSuccess } from "../../stargateclient"; import { defaultGasPrice, defaultSigningClientOptions, diff --git a/packages/stargate/src/modules/staking/queries.spec.ts b/packages/stargate/src/modules/staking/queries.spec.ts index b8ce620b20..fff241b97e 100644 --- a/packages/stargate/src/modules/staking/queries.spec.ts +++ b/packages/stargate/src/modules/staking/queries.spec.ts @@ -4,9 +4,9 @@ import { Tendermint34Client } from "@cosmjs/tendermint-rpc"; import { sleep } from "@cosmjs/utils"; import { MsgDelegate, MsgUndelegate } from "cosmjs-types/cosmos/staking/v1beta1/tx"; +import { assertIsDeliverTxSuccess } from "../../client"; import { QueryClient } from "../../queryclient"; import { SigningStargateClient } from "../../signingstargateclient"; -import { assertIsDeliverTxSuccess } from "../../stargateclient"; import { defaultSigningClientOptions, faucet, diff --git a/packages/stargate/src/modules/tx/queries.spec.ts b/packages/stargate/src/modules/tx/queries.spec.ts index e521183ac3..f11d37d46d 100644 --- a/packages/stargate/src/modules/tx/queries.spec.ts +++ b/packages/stargate/src/modules/tx/queries.spec.ts @@ -4,9 +4,10 @@ import { assertDefined, sleep } from "@cosmjs/utils"; import { MsgDelegate } from "cosmjs-types/cosmos/staking/v1beta1/tx"; import Long from "long"; +import { assertIsDeliverTxSuccess } from "../../client"; import { QueryClient } from "../../queryclient"; import { defaultRegistryTypes, SigningStargateClient } from "../../signingstargateclient"; -import { assertIsDeliverTxSuccess, StargateClient } from "../../stargateclient"; +import { StargateClient } from "../../stargateclient"; import { defaultSigningClientOptions, faucet, diff --git a/packages/stargate/src/modules/vesting/messages.spec.ts b/packages/stargate/src/modules/vesting/messages.spec.ts index 5754c410b3..0866b78ea4 100644 --- a/packages/stargate/src/modules/vesting/messages.spec.ts +++ b/packages/stargate/src/modules/vesting/messages.spec.ts @@ -3,8 +3,8 @@ import { DirectSecp256k1HdWallet } from "@cosmjs/proto-signing"; import { MsgCreateVestingAccount } from "cosmjs-types/cosmos/vesting/v1beta1/tx"; import Long from "long"; +import { assertIsDeliverTxSuccess } from "../../client"; import { SigningStargateClient } from "../../signingstargateclient"; -import { assertIsDeliverTxSuccess } from "../../stargateclient"; import { defaultSigningClientOptions, faucet, diff --git a/packages/stargate/src/multisignature.spec.ts b/packages/stargate/src/multisignature.spec.ts index 1609e2ec92..c7f120adde 100644 --- a/packages/stargate/src/multisignature.spec.ts +++ b/packages/stargate/src/multisignature.spec.ts @@ -9,10 +9,11 @@ import { coins } from "@cosmjs/proto-signing"; import { assert } from "@cosmjs/utils"; import { MsgSend } from "cosmjs-types/cosmos/bank/v1beta1/tx"; +import { assertIsDeliverTxSuccess } from "./client"; import { MsgSendEncodeObject } from "./modules"; import { makeCompactBitArray, makeMultisignedTxBytes } from "./multisignature"; import { SignerData, SigningStargateClient } from "./signingstargateclient"; -import { assertIsDeliverTxSuccess, StargateClient } from "./stargateclient"; +import { StargateClient } from "./stargateclient"; import { faucet, pendingWithoutSimapp, simapp } from "./testutils.spec"; describe("multisignature", () => { diff --git a/packages/stargate/src/signingstargateclient.spec.ts b/packages/stargate/src/signingstargateclient.spec.ts index 959aef7a6e..9a1e1a824b 100644 --- a/packages/stargate/src/signingstargateclient.spec.ts +++ b/packages/stargate/src/signingstargateclient.spec.ts @@ -22,6 +22,7 @@ import Long from "long"; import protobuf from "protobufjs/minimal"; import { AminoTypes } from "./aminotypes"; +import { assertIsDeliverTxFailure, assertIsDeliverTxSuccess, isDeliverTxFailure } from "./client"; import { AminoMsgDelegate, MsgDelegateEncodeObject, @@ -34,7 +35,6 @@ import { SigningStargateClient, SigningStargateClientOptions, } from "./signingstargateclient"; -import { assertIsDeliverTxFailure, assertIsDeliverTxSuccess, isDeliverTxFailure } from "./stargateclient"; import { defaultGasPrice, defaultSendFee, diff --git a/packages/stargate/src/signingstargateclient.ts b/packages/stargate/src/signingstargateclient.ts index a064d1649a..e0d63c4a1e 100644 --- a/packages/stargate/src/signingstargateclient.ts +++ b/packages/stargate/src/signingstargateclient.ts @@ -29,6 +29,7 @@ import { Height } from "cosmjs-types/ibc/core/client/v1/client"; import Long from "long"; import { AminoConverters, AminoTypes } from "./aminotypes"; +import { DeliverTxResponse } from "./client"; import { calculateFee, GasPrice } from "./fee"; import { authzTypes, @@ -56,7 +57,7 @@ import { createStakingAminoConverters, createVestingAminoConverters, } from "./modules"; -import { DeliverTxResponse, StargateClient, StargateClientOptions } from "./stargateclient"; +import { StargateClient, StargateClientOptions } from "./stargateclient"; export const defaultRegistryTypes: ReadonlyArray<[string, GeneratedType]> = [ ["/cosmos.base.v1beta1.Coin", Coin], diff --git a/packages/stargate/src/stargateclient.searchtx.spec.ts b/packages/stargate/src/stargateclient.searchtx.spec.ts index 57803107ac..6d66674160 100644 --- a/packages/stargate/src/stargateclient.searchtx.spec.ts +++ b/packages/stargate/src/stargateclient.searchtx.spec.ts @@ -14,8 +14,9 @@ import { MsgSendResponse } from "cosmjs-types/cosmos/bank/v1beta1/tx"; import { Coin } from "cosmjs-types/cosmos/base/v1beta1/coin"; import { TxRaw } from "cosmjs-types/cosmos/tx/v1beta1/tx"; +import { DeliverTxResponse, isDeliverTxFailure, isDeliverTxSuccess } from "./client"; import { isMsgSendEncodeObject } from "./modules"; -import { DeliverTxResponse, isDeliverTxFailure, isDeliverTxSuccess, StargateClient } from "./stargateclient"; +import { StargateClient } from "./stargateclient"; import { defaultSigningClientOptions, faucet, diff --git a/packages/stargate/src/stargateclient.spec.ts b/packages/stargate/src/stargateclient.spec.ts index 2ae554465f..701d417674 100644 --- a/packages/stargate/src/stargateclient.spec.ts +++ b/packages/stargate/src/stargateclient.spec.ts @@ -19,10 +19,9 @@ import { DeliverTxResponse, isDeliverTxFailure, isDeliverTxSuccess, - PrivateStargateClient, - StargateClient, TimeoutError, -} from "./stargateclient"; +} from "./client"; +import { PrivateStargateClient, StargateClient } from "./stargateclient"; import { faucet, makeRandomAddress, @@ -98,7 +97,7 @@ describe("StargateClient", () => { pendingWithoutSimapp(); const client = await StargateClient.connect(simapp.tendermintUrl); const openedClient = client as unknown as PrivateStargateClient; - const getCodeSpy = spyOn(openedClient.cometClient!, "status").and.callThrough(); + const getCodeSpy = spyOn(openedClient.client.cometClient!, "status").and.callThrough(); expect(await client.getChainId()).toEqual(simapp.chainId); // from network expect(await client.getChainId()).toEqual(simapp.chainId); // from cache diff --git a/packages/stargate/src/stargateclient.ts b/packages/stargate/src/stargateclient.ts index 69ff4582d3..54d1b1352d 100644 --- a/packages/stargate/src/stargateclient.ts +++ b/packages/stargate/src/stargateclient.ts @@ -1,16 +1,13 @@ /* eslint-disable @typescript-eslint/naming-convention */ import { addCoins } from "@cosmjs/amino"; -import { toHex } from "@cosmjs/encoding"; -import { Uint53 } from "@cosmjs/math"; -import { CometClient, connectComet, HttpEndpoint, toRfc3339WithNanoseconds } from "@cosmjs/tendermint-rpc"; -import { assert, sleep } from "@cosmjs/utils"; -import { MsgData, TxMsgData } from "cosmjs-types/cosmos/base/abci/v1beta1/abci"; +import { CometClient, connectComet, HttpEndpoint } from "@cosmjs/tendermint-rpc"; +import { assert } from "@cosmjs/utils"; import { Coin } from "cosmjs-types/cosmos/base/v1beta1/coin"; import { QueryDelegatorDelegationsResponse } from "cosmjs-types/cosmos/staking/v1beta1/query"; import { DelegationResponse } from "cosmjs-types/cosmos/staking/v1beta1/staking"; -import { Account, accountFromAny, AccountParser } from "./accounts"; -import { Event, fromTendermintEvent } from "./events"; +import { Account, AccountParser } from "./accounts"; +import { Block, Client, DeliverTxResponse, IndexedTx, SequenceResponse } from "./client"; import { AuthExtension, BankExtension, @@ -24,165 +21,11 @@ import { import { QueryClient } from "./queryclient"; import { SearchTxQuery } from "./search"; -export class TimeoutError extends Error { - public readonly txId: string; - - public constructor(message: string, txId: string) { - super(message); - this.txId = txId; - } -} - -export interface BlockHeader { - readonly version: { - readonly block: string; - readonly app: string; - }; - readonly height: number; - readonly chainId: string; - /** An RFC 3339 time string like e.g. '2020-02-15T10:39:10.4696305Z' */ - readonly time: string; -} - -export interface Block { - /** The ID is a hash of the block header (uppercase hex) */ - readonly id: string; - readonly header: BlockHeader; - /** Array of raw transactions */ - readonly txs: readonly Uint8Array[]; -} - -/** A transaction that is indexed as part of the transaction history */ -export interface IndexedTx { - readonly height: number; - /** The position of the transaction within the block. This is a 0-based index. */ - readonly txIndex: number; - /** Transaction hash (might be used as transaction ID). Guaranteed to be non-empty upper-case hex */ - readonly hash: string; - /** Transaction execution error code. 0 on success. */ - readonly code: number; - readonly events: readonly Event[]; - /** - * A string-based log document. - * - * This currently seems to merge attributes of multiple events into one event per type - * (https://github.com/tendermint/tendermint/issues/9595). You might want to use the `events` - * field instead. - */ - readonly rawLog: string; - /** - * Raw transaction bytes stored in Tendermint. - * - * If you hash this, you get the transaction hash (= transaction ID): - * - * ```js - * import { sha256 } from "@cosmjs/crypto"; - * import { toHex } from "@cosmjs/encoding"; - * - * const transactionId = toHex(sha256(indexTx.tx)).toUpperCase(); - * ``` - * - * Use `decodeTxRaw` from @cosmjs/proto-signing to decode this. - */ - readonly tx: Uint8Array; - /** - * The message responses of the [TxMsgData](https://github.com/cosmos/cosmos-sdk/blob/v0.46.3/proto/cosmos/base/abci/v1beta1/abci.proto#L128-L140) - * as `Any`s. - * This field is an empty list for chains running Cosmos SDK < 0.46. - */ - readonly msgResponses: Array<{ readonly typeUrl: string; readonly value: Uint8Array }>; - readonly gasUsed: number; - readonly gasWanted: number; -} - -export interface SequenceResponse { - readonly accountNumber: number; - readonly sequence: number; -} - -/** - * The response after successfully broadcasting a transaction. - * Success or failure refer to the execution result. - */ -export interface DeliverTxResponse { - readonly height: number; - /** The position of the transaction within the block. This is a 0-based index. */ - readonly txIndex: number; - /** Error code. The transaction suceeded iff code is 0. */ - readonly code: number; - readonly transactionHash: string; - readonly events: readonly Event[]; - /** - * A string-based log document. - * - * This currently seems to merge attributes of multiple events into one event per type - * (https://github.com/tendermint/tendermint/issues/9595). You might want to use the `events` - * field instead. - */ - readonly rawLog?: string; - /** @deprecated Use `msgResponses` instead. */ - readonly data?: readonly MsgData[]; - /** - * The message responses of the [TxMsgData](https://github.com/cosmos/cosmos-sdk/blob/v0.46.3/proto/cosmos/base/abci/v1beta1/abci.proto#L128-L140) - * as `Any`s. - * This field is an empty list for chains running Cosmos SDK < 0.46. - */ - readonly msgResponses: Array<{ readonly typeUrl: string; readonly value: Uint8Array }>; - readonly gasUsed: number; - readonly gasWanted: number; -} - -export function isDeliverTxFailure(result: DeliverTxResponse): boolean { - return !!result.code; -} - -export function isDeliverTxSuccess(result: DeliverTxResponse): boolean { - return !isDeliverTxFailure(result); -} - -/** - * Ensures the given result is a success. Throws a detailed error message otherwise. - */ -export function assertIsDeliverTxSuccess(result: DeliverTxResponse): void { - if (isDeliverTxFailure(result)) { - throw new Error( - `Error when broadcasting tx ${result.transactionHash} at height ${result.height}. Code: ${result.code}; Raw log: ${result.rawLog}`, - ); - } -} - -/** - * Ensures the given result is a failure. Throws a detailed error message otherwise. - */ -export function assertIsDeliverTxFailure(result: DeliverTxResponse): void { - if (isDeliverTxSuccess(result)) { - throw new Error( - `Transaction ${result.transactionHash} did not fail at height ${result.height}. Code: ${result.code}; Raw log: ${result.rawLog}`, - ); - } -} - -/** - * An error when broadcasting the transaction. This contains the CheckTx errors - * from the blockchain. Once a transaction is included in a block no BroadcastTxError - * is thrown, even if the execution fails (DeliverTx errors). - */ -export class BroadcastTxError extends Error { - public readonly code: number; - public readonly codespace: string; - public readonly log: string | undefined; - - public constructor(code: number, codespace: string, log: string | undefined) { - super(`Broadcasting transaction failed with code ${code} (codespace: ${codespace}). Log: ${log}`); - this.code = code; - this.codespace = codespace; - this.log = log; - } -} - /** Use for testing only */ export interface PrivateStargateClient { - readonly cometClient: CometClient | undefined; + readonly client: { + readonly cometClient: CometClient | undefined; + }; } export interface StargateClientOptions { @@ -190,12 +33,12 @@ export interface StargateClientOptions { } export class StargateClient { - private readonly cometClient: CometClient | undefined; + private readonly client: Client; + + /** We maintain out own query client since the Client instance does not offer what we need here */ private readonly queryClient: | (QueryClient & AuthExtension & BankExtension & StakingExtension & TxExtension) | undefined; - private chainId: string | undefined; - private readonly accountParser: AccountParser; /** * Creates an instance by connecting to the given Tendermint RPC endpoint. @@ -224,7 +67,6 @@ export class StargateClient { protected constructor(cometClient: CometClient | undefined, options: StargateClientOptions) { if (cometClient) { - this.cometClient = cometClient; this.queryClient = QueryClient.withExtensions( cometClient, setupAuthExtension, @@ -233,19 +75,15 @@ export class StargateClient { setupTxExtension, ); } - const { accountParser = accountFromAny } = options; - this.accountParser = accountParser; + this.client = new Client(cometClient, undefined, options); } protected getTmClient(): CometClient | undefined { - return this.cometClient; + return this.client.getCometClient(); } protected forceGetTmClient(): CometClient { - if (!this.cometClient) { - throw new Error("Comet client not available. You cannot use online functionality in offline mode."); - } - return this.cometClient; + return this.client.forceGetCometClient(); } protected getQueryClient(): @@ -266,14 +104,7 @@ export class StargateClient { } public async getChainId(): Promise { - if (!this.chainId) { - const response = await this.forceGetTmClient().status(); - const chainId = response.nodeInfo.network; - if (!chainId) throw new Error("Chain ID must not be empty"); - this.chainId = chainId; - } - - return this.chainId; + return this.client.getChainId(); } public async getHeight(): Promise { @@ -282,45 +113,15 @@ export class StargateClient { } public async getAccount(searchAddress: string): Promise { - try { - const account = await this.forceGetQueryClient().auth.account(searchAddress); - return account ? this.accountParser(account) : null; - } catch (error: any) { - if (/rpc error: code = NotFound/i.test(error.toString())) { - return null; - } - throw error; - } + return this.client.getAccount(searchAddress); } public async getSequence(address: string): Promise { - const account = await this.getAccount(address); - if (!account) { - throw new Error( - `Account '${address}' does not exist on chain. Send some tokens there before trying to query sequence.`, - ); - } - return { - accountNumber: account.accountNumber, - sequence: account.sequence, - }; + return this.client.getSequence(address); } public async getBlock(height?: number): Promise { - const response = await this.forceGetTmClient().block(height); - return { - id: toHex(response.blockId.hash).toUpperCase(), - header: { - version: { - block: new Uint53(response.block.header.version.block).toString(), - app: new Uint53(response.block.header.version.app).toString(), - }, - height: response.block.header.height, - chainId: response.block.header.chainId, - time: toRfc3339WithNanoseconds(response.block.header.time), - }, - txs: response.block.txs, - }; + return this.client.getBlock(height); } public async getBalance(address: string, searchDenom: string): Promise { @@ -378,24 +179,15 @@ export class StargateClient { } public async getTx(id: string): Promise { - const results = await this.txsQuery(`tx.hash='${id}'`); - return results[0] ?? null; + return this.client.getTx(id); } public async searchTx(query: SearchTxQuery): Promise { - let rawQuery: string; - if (typeof query === "string") { - rawQuery = query; - } else if (Array.isArray(query)) { - rawQuery = query.map((t) => `${t.key}='${t.value}'`).join(" AND "); - } else { - throw new Error("Got unsupported query type. See CosmJS 0.31 CHANGELOG for API breaking changes here."); - } - return this.txsQuery(rawQuery); + return this.client.searchTx(query); } public disconnect(): void { - if (this.cometClient) this.cometClient.disconnect(); + this.client.disconnect(); } /** @@ -414,51 +206,7 @@ export class StargateClient { timeoutMs = 60_000, pollIntervalMs = 3_000, ): Promise { - let timedOut = false; - const txPollTimeout = setTimeout(() => { - timedOut = true; - }, timeoutMs); - - const pollForTx = async (txId: string): Promise => { - if (timedOut) { - throw new TimeoutError( - `Transaction with ID ${txId} was submitted but was not yet found on the chain. You might want to check later. There was a wait of ${ - timeoutMs / 1000 - } seconds.`, - txId, - ); - } - await sleep(pollIntervalMs); - const result = await this.getTx(txId); - return result - ? { - code: result.code, - height: result.height, - txIndex: result.txIndex, - events: result.events, - rawLog: result.rawLog, - transactionHash: txId, - msgResponses: result.msgResponses, - gasUsed: result.gasUsed, - gasWanted: result.gasWanted, - } - : pollForTx(txId); - }; - - const transactionId = await this.broadcastTxSync(tx); - - return new Promise((resolve, reject) => - pollForTx(transactionId).then( - (value) => { - clearTimeout(txPollTimeout); - resolve(value); - }, - (error) => { - clearTimeout(txPollTimeout); - reject(error); - }, - ), - ); + return this.client.broadcastTx(tx, timeoutMs, pollIntervalMs); } /** @@ -473,35 +221,10 @@ export class StargateClient { * @returns Returns the hash of the transaction */ public async broadcastTxSync(tx: Uint8Array): Promise { - const broadcasted = await this.forceGetTmClient().broadcastTxSync({ tx }); - - if (broadcasted.code) { - return Promise.reject( - new BroadcastTxError(broadcasted.code, broadcasted.codespace ?? "", broadcasted.log), - ); - } - - const transactionId = toHex(broadcasted.hash).toUpperCase(); - - return transactionId; + return this.client.broadcastTxSync(tx); } private async txsQuery(query: string): Promise { - const results = await this.forceGetTmClient().txSearchAll({ query: query }); - return results.txs.map((tx): IndexedTx => { - const txMsgData = TxMsgData.decode(tx.result.data ?? new Uint8Array()); - return { - height: tx.height, - txIndex: tx.index, - hash: toHex(tx.hash).toUpperCase(), - code: tx.result.code, - events: tx.result.events.map(fromTendermintEvent), - rawLog: tx.result.log || "", - tx: tx.tx, - msgResponses: txMsgData.msgResponses, - gasUsed: tx.result.gasUsed, - gasWanted: tx.result.gasWanted, - }; - }); + return this.client.txsQuery(query); } }