From f58530f4499a95d502dc60a7fc1ae1b6a06d6dce Mon Sep 17 00:00:00 2001 From: Simon Warta Date: Thu, 12 Oct 2023 18:10:44 +0200 Subject: [PATCH] Let StargateClient build on Client --- packages/stargate/src/client.ts | 230 +++++++++++-- packages/stargate/src/events.ts | 4 +- packages/stargate/src/index.ts | 31 +- .../src/modules/authz/queries.spec.ts | 2 +- .../src/modules/distribution/queries.spec.ts | 2 +- .../stargate/src/modules/gov/messages.spec.ts | 2 +- .../stargate/src/modules/gov/queries.spec.ts | 2 +- .../src/modules/staking/messages.spec.ts | 2 +- .../src/modules/staking/queries.spec.ts | 2 +- .../stargate/src/modules/tx/queries.spec.ts | 3 +- .../src/modules/vesting/messages.spec.ts | 2 +- packages/stargate/src/multisignature.spec.ts | 3 +- .../src/signingstargateclient.spec.ts | 2 +- .../stargate/src/signingstargateclient.ts | 3 +- .../src/stargateclient.searchtx.spec.ts | 3 +- packages/stargate/src/stargateclient.spec.ts | 5 +- packages/stargate/src/stargateclient.ts | 319 ++---------------- 17 files changed, 264 insertions(+), 353 deletions(-) diff --git a/packages/stargate/src/client.ts b/packages/stargate/src/client.ts index 0c3b8b653d..0499bfd40c 100644 --- a/packages/stargate/src/client.ts +++ b/packages/stargate/src/client.ts @@ -11,26 +11,175 @@ import { Registry, TxBodyEncodeObject, } from "@cosmjs/proto-signing"; -import { CometClient, connectComet, HttpEndpoint } from "@cosmjs/tendermint-rpc"; +import { CometClient, connectComet, HttpEndpoint, toRfc3339WithNanoseconds } from "@cosmjs/tendermint-rpc"; import { assert, assertDefined, sleep } from "@cosmjs/utils"; -import { TxMsgData } from "cosmjs-types/cosmos/base/abci/v1beta1/abci"; +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 { fromTendermintEvent } from "./events"; +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"; -import { - BroadcastTxError, - DeliverTxResponse, - IndexedTx, - SequenceResponse, - TimeoutError, -} from "./stargateclient"; + +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. @@ -67,7 +216,7 @@ export class Client { /** Chain ID cache */ private chainId: string | undefined; private readonly accountParser: AccountParser; - private readonly signer: OfflineSigner; + private readonly signer: OfflineSigner | undefined; private readonly aminoTypes: AminoTypes; private readonly gasPrice: GasPrice | undefined; @@ -87,7 +236,7 @@ export class Client { } /** - * Creates an instance from a manually created Tendermint client. + * Creates an instance from a manually created Comet client. * Use this to use `Tendermint37Client` instead of `Tendermint34Client`. */ public static async createWithSigner( @@ -98,6 +247,13 @@ export class Client { 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. * @@ -111,14 +267,21 @@ export class Client { return new Client(undefined, signer, options); } - protected constructor(cometClient: CometClient | undefined, signer: OfflineSigner, options: ClientOptions) { + public constructor( + cometClient: CometClient | undefined, + signer: OfflineSigner | undefined, + options: ClientOptions, + ) { if (cometClient) { this.cometClient = cometClient; this.queryClient = QueryClient.withExtensions(cometClient, setupAuthExtension, setupTxExtension); } - const { accountParser = accountFromAny } = options; + const { + registry = new Registry(), + aminoTypes = new AminoTypes({}), + accountParser = accountFromAny, + } = options; this.accountParser = accountParser; - const { registry = new Registry(), aminoTypes = new AminoTypes({}) } = options; this.registry = registry; this.aminoTypes = aminoTypes; this.signer = signer; @@ -127,11 +290,11 @@ export class Client { this.gasPrice = options.gasPrice; } - protected getTmClient(): CometClient | undefined { + public getCometClient(): CometClient | undefined { return this.cometClient; } - protected forceGetTmClient(): CometClient { + public forceGetCometClient(): CometClient { if (!this.cometClient) { throw new Error("Comet client not available. You cannot use online functionality in offline mode."); } @@ -174,12 +337,33 @@ export class Client { }; } + 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, ); @@ -195,7 +379,7 @@ export class Client { public async getChainId(): Promise { if (!this.chainId) { - const response = await this.forceGetTmClient().status(); + 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; @@ -269,6 +453,7 @@ export class Client { memo: string, explicitSignerData?: SignerData, ): Promise { + assert(this.signer, "No signer set for this client instance"); let signerData: SignerData; if (explicitSignerData) { signerData = explicitSignerData; @@ -281,7 +466,6 @@ export class Client { chainId: chainId, }; } - return isOfflineDirectSigner(this.signer) ? this.signDirect(signerAddress, messages, fee, memo, signerData) : this.signAmino(signerAddress, messages, fee, memo, signerData); @@ -294,6 +478,7 @@ export class Client { 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, @@ -339,6 +524,7 @@ export class Client { 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, @@ -468,7 +654,7 @@ export class Client { * @returns Returns the hash of the transaction */ public async broadcastTxSync(tx: Uint8Array): Promise { - const broadcasted = await this.forceGetTmClient().broadcastTxSync({ tx }); + const broadcasted = await this.forceGetCometClient().broadcastTxSync({ tx }); if (broadcasted.code) { return Promise.reject( @@ -481,8 +667,8 @@ export class Client { return transactionId; } - private async txsQuery(query: string): Promise { - const results = await this.forceGetTmClient().txSearchAll({ query: query }); + 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 { 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..11a113d257 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, diff --git a/packages/stargate/src/stargateclient.ts b/packages/stargate/src/stargateclient.ts index 69ff4582d3..5f241b67ab 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,162 +21,6 @@ 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; @@ -190,12 +31,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 +65,6 @@ export class StargateClient { protected constructor(cometClient: CometClient | undefined, options: StargateClientOptions) { if (cometClient) { - this.cometClient = cometClient; this.queryClient = QueryClient.withExtensions( cometClient, setupAuthExtension, @@ -233,19 +73,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 +102,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 +111,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 +177,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 +204,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 +219,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); } }