diff --git a/CHANGELOG.md b/CHANGELOG.md index d96c6ae9..5ee56bfa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -50,6 +50,11 @@ and this project adheres to `withdrawRewards` methods to `SigningStargateClient`. - @cosmjs/stargate: Export `defaultGasLimits` and `defaultGasPrice`. - @cosmjs/cosmwasm-stargate: Export `defaultGasLimits`. +- @cosmjs/stargate: `SigningStargateClient` constructor is now `protected`. +- @cosmjs/cosmwasm-stargate: `SigningCosmWasmClient` constructor is now + `protected`. +- @cosmjs/cosmwasm-stargate: Add `SigningCosmWasmClient.offline` static method + for constructing offline clients without a Tendermint client. ### Changed diff --git a/packages/cosmwasm-stargate/src/cosmwasmclient.spec.ts b/packages/cosmwasm-stargate/src/cosmwasmclient.spec.ts index cf8b03b7..ce064371 100644 --- a/packages/cosmwasm-stargate/src/cosmwasmclient.spec.ts +++ b/packages/cosmwasm-stargate/src/cosmwasmclient.spec.ts @@ -57,7 +57,7 @@ describe("CosmWasmClient", () => { pendingWithoutWasmd(); const client = await CosmWasmClient.connect(wasmd.endpoint); const openedClient = (client as unknown) as PrivateCosmWasmClient; - const getCodeSpy = spyOn(openedClient.tmClient, "status").and.callThrough(); + const getCodeSpy = spyOn(openedClient.tmClient!, "status").and.callThrough(); expect(await client.getChainId()).toEqual(wasmd.chainId); // from network expect(await client.getChainId()).toEqual(wasmd.chainId); // from cache @@ -256,7 +256,7 @@ describe("CosmWasmClient", () => { pendingWithoutWasmd(); const client = await CosmWasmClient.connect(wasmd.endpoint); const openedClient = (client as unknown) as PrivateCosmWasmClient; - const getCodeSpy = spyOn(openedClient.queryClient.unverified.wasm, "getCode").and.callThrough(); + const getCodeSpy = spyOn(openedClient.queryClient!.unverified.wasm, "getCode").and.callThrough(); const result1 = await client.getCodeDetails(deployedHackatom.codeId); // from network const result2 = await client.getCodeDetails(deployedHackatom.codeId); // from cache diff --git a/packages/cosmwasm-stargate/src/cosmwasmclient.ts b/packages/cosmwasm-stargate/src/cosmwasmclient.ts index fb2af52d..e0d1aca0 100644 --- a/packages/cosmwasm-stargate/src/cosmwasmclient.ts +++ b/packages/cosmwasm-stargate/src/cosmwasmclient.ts @@ -42,13 +42,13 @@ import { setupWasmExtension, WasmExtension } from "./queries"; /** Use for testing only */ export interface PrivateCosmWasmClient { - readonly tmClient: Tendermint34Client; - readonly queryClient: QueryClient & AuthExtension & BankExtension & WasmExtension; + readonly tmClient: Tendermint34Client | undefined; + readonly queryClient: (QueryClient & AuthExtension & BankExtension & WasmExtension) | undefined; } export class CosmWasmClient { - private readonly tmClient: Tendermint34Client; - private readonly queryClient: QueryClient & AuthExtension & BankExtension & WasmExtension; + private readonly tmClient: Tendermint34Client | undefined; + private readonly queryClient: (QueryClient & AuthExtension & BankExtension & WasmExtension) | undefined; private readonly codesCache = new Map(); private chainId: string | undefined; @@ -57,19 +57,45 @@ export class CosmWasmClient { return new CosmWasmClient(tmClient); } - protected constructor(tmClient: Tendermint34Client) { - this.tmClient = tmClient; - this.queryClient = QueryClient.withExtensions( - tmClient, - setupAuthExtension, - setupBankExtension, - setupWasmExtension, - ); + protected constructor(tmClient: Tendermint34Client | undefined) { + if (tmClient) { + this.tmClient = tmClient; + this.queryClient = QueryClient.withExtensions( + tmClient, + setupAuthExtension, + setupBankExtension, + setupWasmExtension, + ); + } + } + + protected getTmClient(): Tendermint34Client | undefined { + return this.tmClient; + } + + protected forceGetTmClient(): Tendermint34Client { + if (!this.tmClient) { + throw new Error( + "Tendermint client not available. You cannot use online functionality in offline mode.", + ); + } + return this.tmClient; + } + + protected getQueryClient(): (QueryClient & AuthExtension & BankExtension & WasmExtension) | undefined { + return this.queryClient; + } + + protected forceGetQueryClient(): QueryClient & AuthExtension & BankExtension & WasmExtension { + if (!this.queryClient) { + throw new Error("Query client not available. You cannot use online functionality in offline mode."); + } + return this.queryClient; } public async getChainId(): Promise { if (!this.chainId) { - const response = await this.tmClient.status(); + 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; @@ -79,12 +105,12 @@ export class CosmWasmClient { } public async getHeight(): Promise { - const status = await this.tmClient.status(); + const status = await this.forceGetTmClient().status(); return status.syncInfo.latestBlockHeight; } public async getAccount(searchAddress: string): Promise { - const account = await this.queryClient.auth.account(searchAddress); + const account = await this.forceGetQueryClient().auth.account(searchAddress); return account ? accountFromAny(account) : null; } @@ -101,7 +127,7 @@ export class CosmWasmClient { } public async getBlock(height?: number): Promise { - const response = await this.tmClient.block(height); + const response = await this.forceGetTmClient().block(height); return { id: toHex(response.blockId.hash).toUpperCase(), header: { @@ -118,7 +144,7 @@ export class CosmWasmClient { } public async getBalance(address: string, searchDenom: string): Promise { - const balance = await this.queryClient.bank.balance(address, searchDenom); + const balance = await this.forceGetQueryClient().bank.balance(address, searchDenom); return balance ? coinFromProto(balance) : null; } @@ -166,11 +192,11 @@ export class CosmWasmClient { } public disconnect(): void { - this.tmClient.disconnect(); + if (this.tmClient) this.tmClient.disconnect(); } public async broadcastTx(tx: Uint8Array): Promise { - const response = await this.tmClient.broadcastTxCommit({ tx }); + const response = await this.forceGetTmClient().broadcastTxCommit({ tx }); if (broadcastTxCommitSuccess(response)) { return { height: response.height, @@ -197,7 +223,7 @@ export class CosmWasmClient { } public async getCodes(): Promise { - const { codeInfos } = await this.queryClient.unverified.wasm.listCodeInfo(); + const { codeInfos } = await this.forceGetQueryClient().unverified.wasm.listCodeInfo(); return (codeInfos || []).map( (entry: CodeInfoResponse): Code => { assert(entry.creator && entry.codeId && entry.dataHash, "entry incomplete"); @@ -216,7 +242,7 @@ export class CosmWasmClient { const cached = this.codesCache.get(codeId); if (cached) return cached; - const { codeInfo, data } = await this.queryClient.unverified.wasm.getCode(codeId); + const { codeInfo, data } = await this.forceGetQueryClient().unverified.wasm.getCode(codeId); assert( codeInfo && codeInfo.codeId && codeInfo.creator && codeInfo.dataHash && data, "codeInfo missing or incomplete", @@ -234,7 +260,7 @@ export class CosmWasmClient { } public async getContracts(codeId: number): Promise { - const { contractInfos } = await this.queryClient.unverified.wasm.listContractsByCodeId(codeId); + const { contractInfos } = await this.forceGetQueryClient().unverified.wasm.listContractsByCodeId(codeId); return (contractInfos || []).map( ({ address, contractInfo }): Contract => { assert(address, "address missing"); @@ -260,7 +286,7 @@ export class CosmWasmClient { const { address: retrievedAddress, contractInfo, - } = await this.queryClient.unverified.wasm.getContractInfo(address); + } = await this.forceGetQueryClient().unverified.wasm.getContractInfo(address); if (!contractInfo) throw new Error(`No contract found at address "${address}"`); assert(retrievedAddress, "address missing"); assert(contractInfo.codeId && contractInfo.creator && contractInfo.label, "contractInfo incomplete"); @@ -277,7 +303,7 @@ export class CosmWasmClient { * Throws an error if no contract was found at the address */ public async getContractCodeHistory(address: string): Promise { - const result = await this.queryClient.unverified.wasm.getContractCodeHistory(address); + const result = await this.forceGetQueryClient().unverified.wasm.getContractCodeHistory(address); if (!result) throw new Error(`No contract history found for address "${address}"`); const operations: Record = { [ContractCodeHistoryOperationType.CONTRACT_CODE_HISTORY_OPERATION_TYPE_INIT]: "Init", @@ -306,7 +332,7 @@ export class CosmWasmClient { // just test contract existence await this.getContract(address); - const { data } = await this.queryClient.unverified.wasm.queryContractRaw(address, key); + const { data } = await this.forceGetQueryClient().unverified.wasm.queryContractRaw(address, key); return data ?? null; } @@ -319,7 +345,7 @@ export class CosmWasmClient { */ public async queryContractSmart(address: string, queryMsg: Record): Promise { try { - return await this.queryClient.unverified.wasm.queryContractSmart(address, queryMsg); + return await this.forceGetQueryClient().unverified.wasm.queryContractSmart(address, queryMsg); } catch (error) { if (error instanceof Error) { if (error.message.startsWith("not found: contract")) { @@ -334,7 +360,7 @@ export class CosmWasmClient { } private async txsQuery(query: string): Promise { - const results = await this.tmClient.txSearchAll({ query: query }); + const results = await this.forceGetTmClient().txSearchAll({ query: query }); return results.txs.map((tx) => { return { height: tx.height, diff --git a/packages/cosmwasm-stargate/src/signingcosmwasmclient.ts b/packages/cosmwasm-stargate/src/signingcosmwasmclient.ts index 02b0c765..9184905a 100644 --- a/packages/cosmwasm-stargate/src/signingcosmwasmclient.ts +++ b/packages/cosmwasm-stargate/src/signingcosmwasmclient.ts @@ -133,8 +133,24 @@ export class SigningCosmWasmClient extends CosmWasmClient { return new SigningCosmWasmClient(tmClient, signer, options); } - private constructor( - tmClient: Tendermint34Client, + /** + * 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: SigningCosmWasmClientOptions = {}, + ): Promise { + return new SigningCosmWasmClient(undefined, signer, options); + } + + protected constructor( + tmClient: Tendermint34Client | undefined, signer: OfflineSigner, options: SigningCosmWasmClientOptions, ) { diff --git a/packages/stargate/src/signingstargateclient.ts b/packages/stargate/src/signingstargateclient.ts index b12e65d5..0732a8b6 100644 --- a/packages/stargate/src/signingstargateclient.ts +++ b/packages/stargate/src/signingstargateclient.ts @@ -171,7 +171,7 @@ export class SigningStargateClient extends StargateClient { return new SigningStargateClient(undefined, signer, options); } - private constructor( + protected constructor( tmClient: Tendermint34Client | undefined, signer: OfflineSigner, options: SigningStargateClientOptions, diff --git a/packages/stargate/src/stargateclient.spec.ts b/packages/stargate/src/stargateclient.spec.ts index eef1a218..c28e67b0 100644 --- a/packages/stargate/src/stargateclient.spec.ts +++ b/packages/stargate/src/stargateclient.spec.ts @@ -45,7 +45,7 @@ describe("StargateClient", () => { pendingWithoutSimapp(); const client = await StargateClient.connect(simapp.tendermintUrl); const openedClient = (client as unknown) as PrivateStargateClient; - const getCodeSpy = spyOn(openedClient.tmClient, "status").and.callThrough(); + const getCodeSpy = spyOn(openedClient.tmClient!, "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 8ee069bd..2869a0b4 100644 --- a/packages/stargate/src/stargateclient.ts +++ b/packages/stargate/src/stargateclient.ts @@ -110,7 +110,7 @@ export function coinFromProto(input: Coin): Coin { /** Use for testing only */ export interface PrivateStargateClient { - readonly tmClient: Tendermint34Client; + readonly tmClient: Tendermint34Client | undefined; } export class StargateClient { @@ -130,6 +130,10 @@ export class StargateClient { } } + protected getTmClient(): Tendermint34Client | undefined { + return this.tmClient; + } + protected forceGetTmClient(): Tendermint34Client { if (!this.tmClient) { throw new Error( @@ -139,6 +143,10 @@ export class StargateClient { return this.tmClient; } + protected getQueryClient(): (QueryClient & AuthExtension & BankExtension) | undefined { + return this.queryClient; + } + protected forceGetQueryClient(): QueryClient & AuthExtension & BankExtension { if (!this.queryClient) { throw new Error("Query client not available. You cannot use online functionality in offline mode.");