cosmwasm-stargate: Add CosmWasmClient
This commit is contained in:
parent
91405d2834
commit
aa2b61bd3b
342
packages/cosmwasm-stargate/src/cosmwasmclient.ts
Normal file
342
packages/cosmwasm-stargate/src/cosmwasmclient.ts
Normal file
@ -0,0 +1,342 @@
|
||||
/* eslint-disable @typescript-eslint/naming-convention */
|
||||
import { Code, CodeDetails, Contract, ContractCodeHistoryEntry, JsonObject } from "@cosmjs/cosmwasm";
|
||||
import { fromAscii, toHex } from "@cosmjs/encoding";
|
||||
import {
|
||||
Block,
|
||||
Coin,
|
||||
isSearchByHeightQuery,
|
||||
isSearchByIdQuery,
|
||||
isSearchBySentFromOrToQuery,
|
||||
isSearchByTagsQuery,
|
||||
SearchTxFilter,
|
||||
SearchTxQuery,
|
||||
} from "@cosmjs/launchpad";
|
||||
import { Uint53 } from "@cosmjs/math";
|
||||
import {
|
||||
Account,
|
||||
accountFromProto,
|
||||
AuthExtension,
|
||||
BankExtension,
|
||||
BroadcastTxResponse,
|
||||
codec,
|
||||
coinFromProto,
|
||||
IndexedTx,
|
||||
QueryClient,
|
||||
SequenceResponse,
|
||||
setupAuthExtension,
|
||||
setupBankExtension,
|
||||
} from "@cosmjs/stargate";
|
||||
import {
|
||||
adaptor34,
|
||||
broadcastTxCommitSuccess,
|
||||
Client as TendermintClient,
|
||||
QueryString,
|
||||
} from "@cosmjs/tendermint-rpc";
|
||||
import { assert } from "@cosmjs/utils";
|
||||
|
||||
import { cosmwasm } from "./codec";
|
||||
import { setupWasmExtension, WasmExtension } from "./queries";
|
||||
|
||||
type ICodeInfoResponse = cosmwasm.wasm.v1beta1.ICodeInfoResponse;
|
||||
type ContractCodeHistoryOperationType = cosmwasm.wasm.v1beta1.ContractCodeHistoryOperationType;
|
||||
|
||||
const { TxMsgData } = codec.cosmos.base.abci.v1beta1;
|
||||
const { ContractCodeHistoryOperationType } = cosmwasm.wasm.v1beta1;
|
||||
|
||||
/** Use for testing only */
|
||||
export interface PrivateCosmWasmClient {
|
||||
readonly tmClient: TendermintClient;
|
||||
readonly queryClient: QueryClient & AuthExtension & BankExtension & WasmExtension;
|
||||
}
|
||||
|
||||
export class CosmWasmClient {
|
||||
private readonly tmClient: TendermintClient;
|
||||
private readonly queryClient: QueryClient & AuthExtension & BankExtension & WasmExtension;
|
||||
private readonly codesCache = new Map<number, CodeDetails>();
|
||||
private chainId: string | undefined;
|
||||
|
||||
public static async connect(endpoint: string): Promise<CosmWasmClient> {
|
||||
const tmClient = await TendermintClient.connect(endpoint, adaptor34);
|
||||
return new CosmWasmClient(tmClient);
|
||||
}
|
||||
|
||||
protected constructor(tmClient: TendermintClient) {
|
||||
this.tmClient = tmClient;
|
||||
this.queryClient = QueryClient.withExtensions(
|
||||
tmClient,
|
||||
setupAuthExtension,
|
||||
setupBankExtension,
|
||||
setupWasmExtension,
|
||||
);
|
||||
}
|
||||
|
||||
public async getChainId(): Promise<string> {
|
||||
if (!this.chainId) {
|
||||
const response = await this.tmClient.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 getHeight(): Promise<number> {
|
||||
const status = await this.tmClient.status();
|
||||
return status.syncInfo.latestBlockHeight;
|
||||
}
|
||||
|
||||
public async getAccount(searchAddress: string): Promise<Account | null> {
|
||||
const account = await this.queryClient.auth.account(searchAddress);
|
||||
return account ? accountFromProto(account) : null;
|
||||
}
|
||||
|
||||
public async getSequence(address: string): Promise<SequenceResponse | null> {
|
||||
const account = await this.getAccount(address);
|
||||
if (account) {
|
||||
return {
|
||||
accountNumber: account.accountNumber,
|
||||
sequence: account.sequence,
|
||||
};
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public async getBlock(height?: number): Promise<Block> {
|
||||
const response = await this.tmClient.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: response.block.header.time.toISOString(),
|
||||
},
|
||||
txs: response.block.txs,
|
||||
};
|
||||
}
|
||||
|
||||
public async getBalance(address: string, searchDenom: string): Promise<Coin | null> {
|
||||
const balance = await this.queryClient.bank.balance(address, searchDenom);
|
||||
return balance ? coinFromProto(balance) : null;
|
||||
}
|
||||
|
||||
public async searchTx(query: SearchTxQuery, filter: SearchTxFilter = {}): Promise<readonly IndexedTx[]> {
|
||||
const minHeight = filter.minHeight || 0;
|
||||
const maxHeight = filter.maxHeight || Number.MAX_SAFE_INTEGER;
|
||||
|
||||
if (maxHeight < minHeight) return []; // optional optimization
|
||||
|
||||
let txs: readonly IndexedTx[];
|
||||
|
||||
if (isSearchByIdQuery(query)) {
|
||||
txs = await this.txsQuery(`tx.hash='${query.id}'`);
|
||||
} else if (isSearchByHeightQuery(query)) {
|
||||
txs =
|
||||
query.height >= minHeight && query.height <= maxHeight
|
||||
? await this.txsQuery(`tx.height=${query.height}`)
|
||||
: [];
|
||||
} else if (isSearchBySentFromOrToQuery(query)) {
|
||||
throw new Error(
|
||||
"This type of search query is not yet implemented. See https://github.com/cosmos/cosmjs/issues/533.",
|
||||
);
|
||||
} else if (isSearchByTagsQuery(query)) {
|
||||
throw new Error(
|
||||
"This type of search query is not yet implemented. See https://github.com/cosmos/cosmjs/issues/532.",
|
||||
);
|
||||
} else {
|
||||
throw new Error("Unknown query type");
|
||||
}
|
||||
|
||||
const filtered = txs.filter((tx) => tx.height >= minHeight && tx.height <= maxHeight);
|
||||
return filtered;
|
||||
}
|
||||
|
||||
public disconnect(): void {
|
||||
this.tmClient.disconnect();
|
||||
}
|
||||
|
||||
public async broadcastTx(tx: Uint8Array): Promise<BroadcastTxResponse> {
|
||||
const response = await this.tmClient.broadcastTxCommit({ tx });
|
||||
if (broadcastTxCommitSuccess(response)) {
|
||||
return {
|
||||
height: response.height,
|
||||
transactionHash: toHex(response.hash).toUpperCase(),
|
||||
rawLog: response.deliverTx?.log,
|
||||
data: response.deliverTx?.data ? TxMsgData.decode(response.deliverTx?.data).data : undefined,
|
||||
};
|
||||
}
|
||||
return response.checkTx.code !== 0
|
||||
? {
|
||||
height: response.height,
|
||||
code: response.checkTx.code,
|
||||
transactionHash: toHex(response.hash).toUpperCase(),
|
||||
rawLog: response.checkTx.log,
|
||||
data: response.checkTx.data ? TxMsgData.decode(response.checkTx.data).data : undefined,
|
||||
}
|
||||
: {
|
||||
height: response.height,
|
||||
code: response.deliverTx?.code,
|
||||
transactionHash: toHex(response.hash).toUpperCase(),
|
||||
rawLog: response.deliverTx?.log,
|
||||
data: response.deliverTx?.data ? TxMsgData.decode(response.deliverTx?.data).data : undefined,
|
||||
};
|
||||
}
|
||||
|
||||
public async getCodes(): Promise<readonly Code[]> {
|
||||
const { codeInfos } = await this.queryClient.unverified.wasm.listCodeInfo();
|
||||
return (codeInfos || []).map(
|
||||
(entry: ICodeInfoResponse): Code => {
|
||||
assert(entry.creator && entry.codeId && entry.dataHash, "entry incomplete");
|
||||
return {
|
||||
id: entry.codeId.toNumber(),
|
||||
creator: entry.creator,
|
||||
checksum: toHex(entry.dataHash),
|
||||
source: entry.source || undefined,
|
||||
builder: entry.builder || undefined,
|
||||
};
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
public async getCodeDetails(codeId: number): Promise<CodeDetails> {
|
||||
const cached = this.codesCache.get(codeId);
|
||||
if (cached) return cached;
|
||||
|
||||
const { codeInfo, data } = await this.queryClient.unverified.wasm.getCode(codeId);
|
||||
assert(
|
||||
codeInfo && codeInfo.codeId && codeInfo.creator && codeInfo.dataHash && data,
|
||||
"codeInfo missing or incomplete",
|
||||
);
|
||||
const codeDetails: CodeDetails = {
|
||||
id: codeInfo.codeId.toNumber(),
|
||||
creator: codeInfo.creator,
|
||||
checksum: toHex(codeInfo.dataHash),
|
||||
source: codeInfo.source || undefined,
|
||||
builder: codeInfo.builder || undefined,
|
||||
data: data,
|
||||
};
|
||||
this.codesCache.set(codeId, codeDetails);
|
||||
return codeDetails;
|
||||
}
|
||||
|
||||
public async getContracts(codeId: number): Promise<readonly Contract[]> {
|
||||
const { contractInfos } = await this.queryClient.unverified.wasm.listContractsByCodeId(codeId);
|
||||
return (contractInfos || []).map(
|
||||
({ address, contractInfo }): Contract => {
|
||||
assert(address, "address missing");
|
||||
assert(
|
||||
contractInfo && contractInfo.codeId && contractInfo.creator && contractInfo.label,
|
||||
"contractInfo missing or incomplete",
|
||||
);
|
||||
return {
|
||||
address: address,
|
||||
codeId: contractInfo.codeId.toNumber(),
|
||||
creator: contractInfo.creator,
|
||||
admin: contractInfo.admin || undefined,
|
||||
label: contractInfo.label,
|
||||
};
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Throws an error if no contract was found at the address
|
||||
*/
|
||||
public async getContract(address: string): Promise<Contract> {
|
||||
const {
|
||||
address: retrievedAddress,
|
||||
contractInfo,
|
||||
} = await this.queryClient.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");
|
||||
return {
|
||||
address: retrievedAddress,
|
||||
codeId: contractInfo.codeId.toNumber(),
|
||||
creator: contractInfo.creator,
|
||||
admin: contractInfo.admin || undefined,
|
||||
label: contractInfo.label,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Throws an error if no contract was found at the address
|
||||
*/
|
||||
public async getContractCodeHistory(address: string): Promise<readonly ContractCodeHistoryEntry[]> {
|
||||
const result = await this.queryClient.unverified.wasm.getContractCodeHistory(address);
|
||||
if (!result) throw new Error(`No contract history found for address "${address}"`);
|
||||
const operations: Record<number, "Init" | "Genesis" | "Migrate"> = {
|
||||
[ContractCodeHistoryOperationType.CONTRACT_CODE_HISTORY_OPERATION_TYPE_INIT]: "Init",
|
||||
[ContractCodeHistoryOperationType.CONTRACT_CODE_HISTORY_OPERATION_TYPE_GENESIS]: "Genesis",
|
||||
[ContractCodeHistoryOperationType.CONTRACT_CODE_HISTORY_OPERATION_TYPE_MIGRATE]: "Migrate",
|
||||
};
|
||||
return (result.entries || []).map(
|
||||
(entry): ContractCodeHistoryEntry => {
|
||||
assert(entry.operation && entry.codeId && entry.msg);
|
||||
return {
|
||||
operation: operations[entry.operation],
|
||||
codeId: entry.codeId.toNumber(),
|
||||
msg: JSON.parse(fromAscii(entry.msg)),
|
||||
};
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the data at the key if present (raw contract dependent storage data)
|
||||
* or null if no data at this key.
|
||||
*
|
||||
* Promise is rejected when contract does not exist.
|
||||
*/
|
||||
public async queryContractRaw(address: string, key: Uint8Array): Promise<Uint8Array | null> {
|
||||
// just test contract existence
|
||||
await this.getContract(address);
|
||||
|
||||
const { data } = await this.queryClient.unverified.wasm.queryContractRaw(address, key);
|
||||
return data ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Makes a smart query on the contract, returns the parsed JSON document.
|
||||
*
|
||||
* Promise is rejected when contract does not exist.
|
||||
* Promise is rejected for invalid query format.
|
||||
* Promise is rejected for invalid response format.
|
||||
*/
|
||||
public async queryContractSmart(address: string, queryMsg: Record<string, unknown>): Promise<JsonObject> {
|
||||
try {
|
||||
return await this.queryClient.unverified.wasm.queryContractSmart(address, queryMsg);
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
if (error.message.startsWith("not found: contract")) {
|
||||
throw new Error(`No contract found at address "${address}"`);
|
||||
} else {
|
||||
throw error;
|
||||
}
|
||||
} else {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async txsQuery(query: string): Promise<readonly IndexedTx[]> {
|
||||
const params = {
|
||||
query: query as QueryString,
|
||||
};
|
||||
const results = await this.tmClient.txSearchAll(params);
|
||||
return results.txs.map((tx) => {
|
||||
return {
|
||||
height: tx.height,
|
||||
hash: toHex(tx.hash).toUpperCase(),
|
||||
code: tx.result.code,
|
||||
rawLog: tx.result.log || "",
|
||||
tx: tx.tx,
|
||||
};
|
||||
});
|
||||
}
|
||||
}
|
||||
62
packages/cosmwasm-stargate/types/cosmwasmclient.d.ts
vendored
Normal file
62
packages/cosmwasm-stargate/types/cosmwasmclient.d.ts
vendored
Normal file
@ -0,0 +1,62 @@
|
||||
import { Code, CodeDetails, Contract, ContractCodeHistoryEntry, JsonObject } from "@cosmjs/cosmwasm";
|
||||
import { Block, Coin, SearchTxFilter, SearchTxQuery } from "@cosmjs/launchpad";
|
||||
import {
|
||||
Account,
|
||||
AuthExtension,
|
||||
BankExtension,
|
||||
BroadcastTxResponse,
|
||||
IndexedTx,
|
||||
QueryClient,
|
||||
SequenceResponse,
|
||||
} from "@cosmjs/stargate";
|
||||
import { Client as TendermintClient } from "@cosmjs/tendermint-rpc";
|
||||
import { WasmExtension } from "./queries";
|
||||
/** Use for testing only */
|
||||
export interface PrivateCosmWasmClient {
|
||||
readonly tmClient: TendermintClient;
|
||||
readonly queryClient: QueryClient & AuthExtension & BankExtension & WasmExtension;
|
||||
}
|
||||
export declare class CosmWasmClient {
|
||||
private readonly tmClient;
|
||||
private readonly queryClient;
|
||||
private readonly codesCache;
|
||||
private chainId;
|
||||
static connect(endpoint: string): Promise<CosmWasmClient>;
|
||||
protected constructor(tmClient: TendermintClient);
|
||||
getChainId(): Promise<string>;
|
||||
getHeight(): Promise<number>;
|
||||
getAccount(searchAddress: string): Promise<Account | null>;
|
||||
getSequence(address: string): Promise<SequenceResponse | null>;
|
||||
getBlock(height?: number): Promise<Block>;
|
||||
getBalance(address: string, searchDenom: string): Promise<Coin | null>;
|
||||
searchTx(query: SearchTxQuery, filter?: SearchTxFilter): Promise<readonly IndexedTx[]>;
|
||||
disconnect(): void;
|
||||
broadcastTx(tx: Uint8Array): Promise<BroadcastTxResponse>;
|
||||
getCodes(): Promise<readonly Code[]>;
|
||||
getCodeDetails(codeId: number): Promise<CodeDetails>;
|
||||
getContracts(codeId: number): Promise<readonly Contract[]>;
|
||||
/**
|
||||
* Throws an error if no contract was found at the address
|
||||
*/
|
||||
getContract(address: string): Promise<Contract>;
|
||||
/**
|
||||
* Throws an error if no contract was found at the address
|
||||
*/
|
||||
getContractCodeHistory(address: string): Promise<readonly ContractCodeHistoryEntry[]>;
|
||||
/**
|
||||
* Returns the data at the key if present (raw contract dependent storage data)
|
||||
* or null if no data at this key.
|
||||
*
|
||||
* Promise is rejected when contract does not exist.
|
||||
*/
|
||||
queryContractRaw(address: string, key: Uint8Array): Promise<Uint8Array | null>;
|
||||
/**
|
||||
* Makes a smart query on the contract, returns the parsed JSON document.
|
||||
*
|
||||
* Promise is rejected when contract does not exist.
|
||||
* Promise is rejected for invalid query format.
|
||||
* Promise is rejected for invalid response format.
|
||||
*/
|
||||
queryContractSmart(address: string, queryMsg: Record<string, unknown>): Promise<JsonObject>;
|
||||
private txsQuery;
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user