diff --git a/.vscode/launch.json b/.vscode/launch.json index 9eb7053..aab5bb5 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -36,6 +36,22 @@ "cwd": "${workspaceFolder}/libs/interchainjs", "internalConsoleOptions": "neverOpen" }, + { + "type": "node", + "request": "launch", + "name": "Debug Jest Tests in Interchainjs gov", + "program": "${workspaceFolder}/node_modules/.bin/jest", + "args": [ + "--config", + "./jest.starship.config.js", + "--verbose", + "--bail", + "starship/__tests__/gov.test.ts" + ], + "console": "integratedTerminal", + "cwd": "${workspaceFolder}/libs/interchainjs", + "internalConsoleOptions": "neverOpen" + }, { "type": "node", "request": "launch", diff --git a/libs/interchainjs/src/signing-client.ts b/libs/interchainjs/src/signing-client.ts index 3b5e05b..87d4265 100644 --- a/libs/interchainjs/src/signing-client.ts +++ b/libs/interchainjs/src/signing-client.ts @@ -1,31 +1,25 @@ -import { AminoSigner } from '@interchainjs/cosmos/amino'; -import { DirectSigner } from '@interchainjs/cosmos/direct'; -import { RpcClient } from '@interchainjs/cosmos/query/rpc'; +import { AminoSigner } from "@interchainjs/cosmos/amino"; +import { DirectSigner } from "@interchainjs/cosmos/direct"; +import { RpcClient } from "@interchainjs/cosmos/query/rpc"; import { AminoConverter, Encoder, isICosmosAccount, QueryClient, -} from '@interchainjs/cosmos/types'; +} from "@interchainjs/cosmos/types"; import { isOfflineAminoSigner, isOfflineDirectSigner, OfflineSigner, -} from '@interchainjs/cosmos/types/wallet'; -import { toEncoder } from '@interchainjs/cosmos/utils'; -import { IBinaryWriter } from '@interchainjs/cosmos-types/binary'; -import { PubKey as Secp256k1PubKey } from '@interchainjs/cosmos-types/cosmos/crypto/secp256k1/keys'; -import { TxBody, TxRaw } from '@interchainjs/cosmos-types/cosmos/tx/v1beta1/tx'; -import { Any } from '@interchainjs/cosmos-types/google/protobuf/any'; -import { TxRpc } from '@interchainjs/cosmos-types/types'; -import { - AccountData, - HttpEndpoint, - IKey, - Price, - StdFee, -} from '@interchainjs/types'; -import { fromBase64 } from '@interchainjs/utils'; +} from "@interchainjs/cosmos/types/wallet"; +import { toEncoder } from "@interchainjs/cosmos/utils"; +import { IBinaryWriter } from "@interchainjs/cosmos-types/binary"; +import { PubKey as Secp256k1PubKey } from "@interchainjs/cosmos-types/cosmos/crypto/secp256k1/keys"; +import { TxBody, TxRaw } from "@interchainjs/cosmos-types/cosmos/tx/v1beta1/tx"; +import { Any } from "@interchainjs/cosmos-types/google/protobuf/any"; +import { TxRpc } from "@interchainjs/cosmos-types/types"; +import { BroadcastOptions, HttpEndpoint, IKey, StdFee } from "@interchainjs/types"; +import { fromBase64 } from "@interchainjs/utils"; import { Block, @@ -34,13 +28,13 @@ import { SearchTxQuery, SearchTxResponse, TxResponse, -} from './types/query'; +} from "./types/query"; import { DeliverTxResponse, EncodeObject, SignerOptions, -} from './types/signing-client'; -import { BroadcastTxError } from './utils'; +} from "./types/signing-client"; +import { BroadcastTxError } from "./utils"; /** * implement the same methods as what in `cosmjs` signingClient @@ -58,9 +52,6 @@ export class SigningClient { readonly encoders: Encoder[] = []; readonly converters: AminoConverter[] = []; - private readonly gasPrice?: Price | string; - - private _endpoint: string | HttpEndpoint; protected txRpc: TxRpc; constructor( @@ -78,7 +69,7 @@ export class SigningClient { this.txRpc = { request(): Promise { - throw new Error('Not implemented yet'); + throw new Error("Not implemented yet"); }, signAndBroadcast: this.signAndBroadcast, }; @@ -140,44 +131,6 @@ export class SigningClient { } } - private async getAccountData(address: string): Promise { - const accounts = await this.offlineSigner.getAccounts(); - const account = accounts.find((account) => account.address === address); - if (!account) { - throw new Error( - `No such account found in OfflineSigner for address ${address}` - ); - } - - return isICosmosAccount(account) ? account.toAccountData() : account; - } - - private async getPubkey(address: string): Promise { - const account = await this.getAccountData(address); - let typeUrl: string; - let PubKey: { - encode( - message: { key: Uint8Array }, - writer?: IBinaryWriter - ): IBinaryWriter; - }; - switch (account.algo) { - case 'secp256k1': - typeUrl = Secp256k1PubKey.typeUrl; - PubKey = Secp256k1PubKey; - break; - default: - throw new Error(`${account.algo} not supported.`); - } - const publicKey: Any = { - typeUrl, - value: PubKey.encode({ - key: account.pubkey, - }).finish(), - }; - return publicKey; - } - private get queryClient() { return this.client; } @@ -225,10 +178,10 @@ export class SigningClient { private signWithAutoFee = async ( signerAddress: string, messages: EncodeObject[], - fee: StdFee | 'auto', - memo = '' + fee: StdFee | "auto", + memo = "" ): Promise => { - const usedFee = fee === 'auto' ? undefined : fee; + const usedFee = fee === "auto" ? undefined : fee; return await this.sign(signerAddress, messages, usedFee, memo); }; @@ -257,7 +210,7 @@ export class SigningClient { return Promise.reject( new BroadcastTxError( broadcasted.check_tx?.code, - broadcasted.check_tx?.codespace ?? '', + broadcasted.check_tx?.codespace ?? "", broadcasted.check_tx?.log ) ); @@ -269,8 +222,8 @@ export class SigningClient { public async signAndBroadcastSync( signerAddress: string, messages: EncodeObject[], - fee: StdFee | 'auto', - memo = '' + fee: StdFee | "auto", + memo = "" ): Promise { const txRaw = await this.signWithAutoFee( signerAddress, @@ -284,34 +237,28 @@ export class SigningClient { public async broadcastTx( tx: Uint8Array, - timeoutMs = 60_000, - pollIntervalMs = 3_000 + broadcast: BroadcastOptions ): Promise { - const resp = await this.queryClient.broadcast(tx, { - checkTx: true, - deliverTx: true, - timeoutMs, - pollIntervalMs, - }); + const resp = await this.queryClient.broadcast(tx, broadcast); return { - height: Number(resp.deliver_tx.height), - txIndex: resp.deliver_tx.txIndex, - code: resp.deliver_tx.code, - transactionHash: resp.hash, - events: resp.deliver_tx.events, - rawLog: resp.deliver_tx.rawLog, - msgResponses: resp.deliver_tx.msgResponses, - gasUsed: BigInt(resp.deliver_tx.gas_used), - gasWanted: BigInt(resp.deliver_tx.gas_wanted), + height: resp?.deliver_tx?.height ? Number(resp.deliver_tx.height) : 0, + txIndex: resp?.deliver_tx?.txIndex, + code: resp?.deliver_tx?.code, + transactionHash: resp?.hash, + events: resp?.deliver_tx?.events, + rawLog: resp?.deliver_tx?.rawLog, + msgResponses: resp?.deliver_tx?.msgResponses, + gasUsed: BigInt(resp?.deliver_tx?.gas_used ?? 0), + gasWanted: BigInt(resp?.deliver_tx?.gas_wanted ?? 0), }; } signAndBroadcast = async ( signerAddress: string, messages: EncodeObject[], - fee: StdFee | 'auto', - memo = '' + fee: StdFee | "auto", + memo = "" ): Promise => { const txRaw = await this.signWithAutoFee( signerAddress, @@ -322,8 +269,7 @@ export class SigningClient { const txBytes = TxRaw.encode(txRaw).finish(); return this.broadcastTx( txBytes, - this.options.broadcastTimeoutMs, - this.options.broadcastPollIntervalMs + this.options.broadcast, ); }; @@ -334,7 +280,7 @@ export class SigningClient { async getTx(id: string): Promise { const data = await fetch(`${this.endpoint.url}/tx?hash=0x${id}`); const json = await data.json(); - const tx: TxResponse = json['result']; + const tx: TxResponse = json["result"]; if (!tx) return null; const txRaw = TxRaw.decode(fromBase64(tx.tx)); const txBody = TxBody.decode(txRaw.bodyBytes); @@ -347,28 +293,28 @@ export class SigningClient { rawLog: tx.tx_result.log, tx: fromBase64(tx.tx), msgResponses: txBody.messages, - gasUsed: BigInt(tx.tx_result.gas_used), - gasWanted: BigInt(tx.tx_result.gas_wanted), + gasUsed: tx?.tx_result?.gas_used ? BigInt(tx?.tx_result?.gas_used) : 0n, + gasWanted: tx?.tx_result?.gas_wanted ? BigInt(tx?.tx_result?.gas_wanted) : 0n, }; } async searchTx(query: SearchTxQuery): Promise { let rawQuery: string; - if (typeof query === 'string') { + if (typeof query === "string") { rawQuery = query; } else if (Array.isArray(query)) { - rawQuery = query.map((t) => `${t.key}=${t.value}`).join(' AND '); + rawQuery = query.map((t) => `${t.key}=${t.value}`).join(" AND "); } else { - throw new Error('Got unsupported query type.'); + throw new Error("Got unsupported query type."); } - const orderBy: 'asc' | 'desc' = 'asc'; + const orderBy: "asc" | "desc" = "asc"; const data = await fetch( `${this.endpoint.url}/tx_search?query="${rawQuery}"&order_by="${orderBy}"` // `${this.endpoint.url}/tx_search?query="${rawQuery}"&order_by="${orderBy}"&page=1&per_page=100` ); const json = await data.json(); - const { txs }: SearchTxResponse = json['result']; + const { txs }: SearchTxResponse = json["result"]; return txs.map((tx) => { return { height: Number.parseInt(tx.height), @@ -377,11 +323,11 @@ export class SigningClient { code: 0, // events: tx.tx_result.tags, events: [], - rawLog: tx.tx_result.log || '', + rawLog: tx.tx_result.log || "", tx: fromBase64(tx.tx), msgResponses: [], - gasUsed: BigInt(tx.tx_result.gas_used), - gasWanted: BigInt(tx.tx_result.gas_wanted), + gasUsed: tx?.tx_result?.gas_used ? BigInt(tx.tx_result.gas_used) : 0n, + gasWanted: tx?.tx_result?.gas_wanted ? BigInt(tx.tx_result.gas_wanted) : 0n, } as IndexedTx; }); } @@ -393,7 +339,7 @@ export class SigningClient { : `${this.endpoint.url}/block` ); const json = await data.json(); - const { block_id, block }: BlockResponse = json['result']; + const { block_id, block }: BlockResponse = json["result"]; return { id: block_id.hash.toUpperCase(), header: { diff --git a/libs/interchainjs/src/types/signing-client.ts b/libs/interchainjs/src/types/signing-client.ts index 764246a..dbf573a 100644 --- a/libs/interchainjs/src/types/signing-client.ts +++ b/libs/interchainjs/src/types/signing-client.ts @@ -1,5 +1,5 @@ import { AminoConverter, Message } from '@interchainjs/cosmos/types'; -import { Price } from '@interchainjs/types'; +import { BroadcastOptions, Price } from '@interchainjs/types'; import { Event, TelescopeGeneratedType } from '@interchainjs/types'; export type EncodeObject = Message; @@ -11,10 +11,9 @@ export type Registry = Array<[TypeUrl, TelescopeGeneratedType]>; export interface SignerOptions { registry?: Registry; aminoConverters?: Record; - broadcastTimeoutMs?: number; - broadcastPollIntervalMs?: number; gasPrice?: Price | string; prefix?: string; + broadcast?: BroadcastOptions; } export interface SignerData { diff --git a/libs/interchainjs/starship/__tests__/gov.test.ts b/libs/interchainjs/starship/__tests__/gov.test.ts index 3dc4894..e4cbd70 100644 --- a/libs/interchainjs/starship/__tests__/gov.test.ts +++ b/libs/interchainjs/starship/__tests__/gov.test.ts @@ -111,7 +111,14 @@ describe('Governance tests for osmosis', () => { it('stake tokens to genesis validator', async () => { const signingClient = await StargateSigningClient.connectWithSigner( await getRpcEndpoint(), - directSigner + directSigner, + { + broadcast: { + checkTx: true, + deliverTx: true, + useLegacyBroadcastTxCommit: true, + }, + } ); const { balance } = await queryClient.balance({ @@ -155,7 +162,14 @@ describe('Governance tests for osmosis', () => { it('submit a txt proposal', async () => { const signingClient = await StargateSigningClient.connectWithSigner( await getRpcEndpoint(), - directSigner + directSigner, + { + broadcast: { + checkTx: true, + deliverTx: true, + useLegacyBroadcastTxCommit: true, + }, + } ); const contentMsg = TextProposal.fromPartial({ @@ -223,7 +237,14 @@ describe('Governance tests for osmosis', () => { // create direct address signing client const signingClient = await StargateSigningClient.connectWithSigner( await getRpcEndpoint(), - directSigner + directSigner, + { + broadcast: { + checkTx: true, + deliverTx: true, + useLegacyBroadcastTxCommit: true, + }, + } ); // Vote on proposal from genesis mnemonic address @@ -269,7 +290,14 @@ describe('Governance tests for osmosis', () => { // create amino address signing client const signingClient = await StargateSigningClient.connectWithSigner( await getRpcEndpoint(), - aminoSigner + aminoSigner, + { + broadcast: { + checkTx: true, + deliverTx: true, + useLegacyBroadcastTxCommit: true, + }, + } ); // Vote on proposal from genesis mnemonic address diff --git a/libs/interchainjs/starship/__tests__/staking.test.ts b/libs/interchainjs/starship/__tests__/staking.test.ts index 2313812..9d22748 100644 --- a/libs/interchainjs/starship/__tests__/staking.test.ts +++ b/libs/interchainjs/starship/__tests__/staking.test.ts @@ -85,7 +85,13 @@ describe('Staking tokens testing', () => { it('stake tokens to genesis validator', async () => { const signingClient = await StargateSigningClient.connectWithSigner( await getRpcEndpoint(), - protoSigner + protoSigner, + { + broadcast: { + checkTx: true, + deliverTx: true, + }, + } ); const { balance } = await queryClient.balance({ diff --git a/libs/interchainjs/starship/__tests__/token.test.ts b/libs/interchainjs/starship/__tests__/token.test.ts index 46c97ac..ced2cf3 100644 --- a/libs/interchainjs/starship/__tests__/token.test.ts +++ b/libs/interchainjs/starship/__tests__/token.test.ts @@ -56,7 +56,13 @@ describe('Token transfers', () => { it('send osmosis token to address', async () => { const signingClient = await StargateSigningClient.connectWithSigner( await getRpcEndpoint(), - protoSigner + protoSigner, + { + broadcast: { + checkTx: true, + deliverTx: true, + }, + } ); const fee = { @@ -91,7 +97,13 @@ describe('Token transfers', () => { it('send ibc osmo tokens to address on cosmos chain', async () => { const signingClient = await StargateSigningClient.connectWithSigner( await getRpcEndpoint(), - protoSigner + protoSigner, + { + broadcast: { + checkTx: true, + deliverTx: true, + }, + } ); const { chainInfo: cosmosChainInfo, getRpcEndpoint: cosmosRpcEndpoint } = diff --git a/networks/cosmos/src/query/rpc.ts b/networks/cosmos/src/query/rpc.ts index 0e539ea..322cde9 100644 --- a/networks/cosmos/src/query/rpc.ts +++ b/networks/cosmos/src/query/rpc.ts @@ -1,34 +1,35 @@ -import { BaseAccount } from '@interchainjs/cosmos-types/cosmos/auth/v1beta1/auth'; -import { QueryClientImpl as AuthQuery } from '@interchainjs/cosmos-types/cosmos/auth/v1beta1/query.rpc.Query'; -import { SignMode } from '@interchainjs/cosmos-types/cosmos/tx/signing/v1beta1/signing'; -import { ServiceClientImpl as TxQuery } from '@interchainjs/cosmos-types/cosmos/tx/v1beta1/service.rpc.Service'; +import { BaseAccount } from "@interchainjs/cosmos-types/cosmos/auth/v1beta1/auth"; +import { QueryClientImpl as AuthQuery } from "@interchainjs/cosmos-types/cosmos/auth/v1beta1/query.rpc.Query"; +import { SignMode } from "@interchainjs/cosmos-types/cosmos/tx/signing/v1beta1/signing"; +import { ServiceClientImpl as TxQuery } from "@interchainjs/cosmos-types/cosmos/tx/v1beta1/service.rpc.Service"; import { Fee, SignerInfo, Tx, TxBody, TxRaw, -} from '@interchainjs/cosmos-types/cosmos/tx/v1beta1/tx'; -import { BroadcastOptions, HttpEndpoint } from '@interchainjs/types'; -import { fromBase64, isEmpty, toHttpEndpoint } from '@interchainjs/utils'; +} from "@interchainjs/cosmos-types/cosmos/tx/v1beta1/tx"; +import { BroadcastOptions, HttpEndpoint } from "@interchainjs/types"; +import { fromBase64, isEmpty, toHttpEndpoint } from "@interchainjs/utils"; -import { defaultAccountParser, defaultBroadcastOptions } from '../defaults'; +import { defaultAccountParser, defaultBroadcastOptions } from "../defaults"; import { BroadcastMode, BroadcastResponse, EncodedMessage, QueryClient, -} from '../types'; +} from "../types"; import { AsyncCometBroadcastResponse, + CommitCometBroadcastResponse, IndexedTx, Status, SyncCometBroadcastResponse, TimeoutError, TxResponse, -} from '../types/rpc'; -import { constructAuthInfo } from '../utils/direct'; -import { broadcast, createQueryRpc, getPrefix, sleep } from '../utils/rpc'; +} from "../types/rpc"; +import { constructAuthInfo } from "../utils/direct"; +import { broadcast, createQueryRpc, getPrefix, sleep } from "../utils/rpc"; /** * client for cosmos rpc @@ -80,7 +81,11 @@ export class RpcClient implements QueryClient { } // if there's a baseAccount in the account, and it's a BaseAccount, return it - if ('baseAccount' in accountResp.account && accountResp.account.baseAccount && BaseAccount.is(accountResp.account.baseAccount)){ + if ( + "baseAccount" in accountResp.account && + accountResp.account.baseAccount && + BaseAccount.is(accountResp.account.baseAccount) + ) { return accountResp.account.baseAccount; } @@ -94,7 +99,7 @@ export class RpcClient implements QueryClient { protected async getStatus(): Promise { const data = await fetch(`${this.endpoint.url}/status`); const json = await data.json(); - return json['result']; + return json["result"]; } /** @@ -164,7 +169,7 @@ export class RpcClient implements QueryClient { async getTx(id: string): Promise { const data = await fetch(`${this.endpoint.url}/tx?hash=0x${id}`); const json = await data.json(); - const tx: TxResponse = json['result']; + const tx: TxResponse = json["result"]; if (!tx) return null; const txRaw = TxRaw.decode(fromBase64(tx.tx)); const txBody = TxBody.decode(txRaw.bodyBytes); @@ -196,90 +201,115 @@ export class RpcClient implements QueryClient { txBytes: Uint8Array, options?: BroadcastOptions ): Promise { - const { checkTx, deliverTx, timeoutMs, pollIntervalMs } = { + const { + checkTx, + deliverTx, + timeoutMs, + pollIntervalMs, + useLegacyBroadcastTxCommit, + } = { ...defaultBroadcastOptions, ...options, }; const mode: BroadcastMode = checkTx && deliverTx - ? 'broadcast_tx_commit' + ? "broadcast_tx_commit" : checkTx - ? 'broadcast_tx_sync' - : 'broadcast_tx_async'; + ? "broadcast_tx_sync" + : "broadcast_tx_async"; const resp = await broadcast( this.endpoint, - mode === 'broadcast_tx_commit' ? 'broadcast_tx_async' : mode, + mode === "broadcast_tx_commit" && !useLegacyBroadcastTxCommit + ? "broadcast_tx_async" + : mode, txBytes ); switch (mode) { - case 'broadcast_tx_async': - const { hash: hash1, ...rest1 } = resp as AsyncCometBroadcastResponse; - return { - hash: hash1, - add_tx: rest1, - }; - case 'broadcast_tx_sync': - const { hash: hash2, ...rest2 } = resp as SyncCometBroadcastResponse; - return { - hash: hash2, - check_tx: rest2, - }; - case 'broadcast_tx_commit': - let timedOut = false; - const txPollTimeout = setTimeout(() => { - timedOut = true; - }, timeoutMs); + case "broadcast_tx_async": + const { hash: hash1, ...rest1 } = resp as AsyncCometBroadcastResponse; + return { + hash: hash1, + add_tx: rest1, + }; + case "broadcast_tx_sync": + const { hash: hash2, ...rest2 } = resp as SyncCometBroadcastResponse; + return { + hash: hash2, + check_tx: rest2, + }; + case "broadcast_tx_commit": + if (useLegacyBroadcastTxCommit) { + const { + check_tx, + deliver_tx, + height, + hash: hash3, + } = resp as CommitCometBroadcastResponse; - 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 { + hash: hash3, + check_tx, + deliver_tx: { height, ...deliver_tx }, + }; + } else { + let timedOut = false; + const txPollTimeout = setTimeout(() => { + timedOut = true; + }, timeoutMs); - return result - ? { - hash: resp.hash, - deliver_tx: { - code: result.code, - height: result.height.toString(), - txIndex: result.txIndex, - events: result.events, - rawLog: result.rawLog, - msgResponses: result.msgResponses, - gas_used: result.gasUsed.toString(), - gas_wanted: result.gasWanted.toString(), - data: result.data, - log: result.log, - info: result.info, - }, - } - : pollForTx(txId); - }; + 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); - const transactionId = resp.hash.toUpperCase(); + return result + ? { + hash: resp.hash, + deliver_tx: { + code: result.code, + height: result.height.toString(), + txIndex: result.txIndex, + events: result.events, + rawLog: result.rawLog, + msgResponses: result.msgResponses, + gas_used: result.gasUsed.toString(), + gas_wanted: result.gasWanted.toString(), + data: result.data, + log: result.log, + info: result.info, + }, + } + : pollForTx(txId); + }; + + const transactionId = resp.hash.toUpperCase(); - return new Promise((resolve, reject) => - pollForTx(transactionId).then( - (value) => { - clearTimeout(txPollTimeout); - resolve(value); - }, - (error) => { - clearTimeout(txPollTimeout); - reject(error); - } - ) - ); - default: - throw new Error(`Wrong method: ${mode}`); + return new Promise((resolve, reject) => + pollForTx(transactionId).then( + (value) => { + clearTimeout(txPollTimeout); + resolve(value); + }, + (error) => { + clearTimeout(txPollTimeout); + reject(error); + } + ) + ); + } + default: + throw new Error(`Wrong method: ${mode}`); } } } diff --git a/packages/types/src/signer.ts b/packages/types/src/signer.ts index 88ca522..1a77ea7 100644 --- a/packages/types/src/signer.ts +++ b/packages/types/src/signer.ts @@ -67,6 +67,11 @@ export interface BroadcastOptions { * polling interval in milliseconds for checking broadcast_tx_commit result. */ pollIntervalMs?: number; + + /** + * whether to use legacy broadcast_tx_commit result. + */ + useLegacyBroadcastTxCommit?: boolean; } /**