Merge pull request #280 from CosmWasm/extensible-client

Create extensible API client
This commit is contained in:
Simon Warta 2020-07-08 10:08:33 +02:00 committed by GitHub
commit 6e3b0baa0c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
43 changed files with 1577 additions and 940 deletions

View File

@ -15,4 +15,4 @@ const faucetMnemonic =
const faucetAddress = "cosmos1pkptre7fdkl6gfrzlesjjvhxhlc3r4gmmk8rs6";
const pen = await Secp256k1Pen.fromMnemonic(faucetMnemonic);
const client = new RestClient(defaultHttpUrl);
const client = new LcdClient(defaultHttpUrl);

View File

@ -96,10 +96,10 @@ export function main(originalArgs: readonly string[]): void {
"Msg",
"MsgDelegate",
"MsgSend",
"LcdClient",
"Pen",
"PubKey",
"pubkeyToAddress",
"RestClient",
"Secp256k1Pen",
"SigningCosmosClient",
"StdFee",

View File

@ -26,7 +26,7 @@
"format-text": "prettier --write --prose-wrap always --print-width 80 \"./*.md\"",
"lint": "eslint --max-warnings 0 \"**/*.{js,ts}\"",
"lint-fix": "eslint --max-warnings 0 \"**/*.{js,ts}\" --fix",
"move-types": "shx rm -rf ./types/* && shx mv build/types/* ./types && rm -rf ./types/testdata && shx rm -f ./types/*.spec.d.ts",
"move-types": "shx rm -rf ./types/* && shx mv build/types/* ./types && rm -rf ./types/testdata && shx rm -f ./types/*.spec.d.ts && shx rm ./types/**/*.spec.d.ts",
"format-types": "prettier --write --loglevel warn \"./types/**/*.d.ts\"",
"build": "shx rm -rf ./build && tsc && yarn move-types && yarn format-types",
"build-or-skip": "[ -n \"$SKIP_BUILD\" ] || yarn build",

View File

@ -1,10 +1,18 @@
/* eslint-disable @typescript-eslint/camelcase */
import { Coin, coins, CosmosSdkTx, isMsgSend, makeSignBytes, MsgSend, Secp256k1Pen } from "@cosmjs/sdk38";
import {
Coin,
coins,
CosmosSdkTx,
isMsgSend,
LcdClient,
makeSignBytes,
MsgSend,
Secp256k1Pen,
} from "@cosmjs/sdk38";
import { assert, sleep } from "@cosmjs/utils";
import { CosmWasmClient, isPostTxFailure } from "./cosmwasmclient";
import { isMsgExecuteContract, isMsgInstantiateContract } from "./msgs";
import { RestClient } from "./restclient";
import { SigningCosmWasmClient } from "./signingcosmwasmclient";
import {
alice,
@ -50,7 +58,7 @@ describe("CosmWasmClient.searchTx", () => {
const transferAmount = coins(1234567, "ucosm");
const result = await client.sendTokens(recipient, transferAmount);
await sleep(75); // wait until tx is indexed
const txDetails = await new RestClient(wasmd.endpoint).txById(result.transactionHash);
const txDetails = await new LcdClient(wasmd.endpoint).txById(result.transactionHash);
sendSuccessful = {
sender: alice.address0,
recipient: recipient,
@ -68,7 +76,7 @@ describe("CosmWasmClient.searchTx", () => {
};
const result = await client.sendTokens(recipient, [transferAmount]);
await sleep(75); // wait until tx is indexed
const txDetails = await new RestClient(wasmd.endpoint).txById(result.transactionHash);
const txDetails = await new LcdClient(wasmd.endpoint).txById(result.transactionHash);
sendSelfSuccessful = {
sender: alice.address0,
recipient: recipient,
@ -132,7 +140,7 @@ describe("CosmWasmClient.searchTx", () => {
};
const result = await client.execute(hashInstance, msg);
await sleep(75); // wait until tx is indexed
const txDetails = await new RestClient(wasmd.endpoint).txById(result.transactionHash);
const txDetails = await new LcdClient(wasmd.endpoint).txById(result.transactionHash);
execute = {
sender: alice.address0,
contract: hashInstance,

View File

@ -54,7 +54,7 @@ describe("CosmWasmClient", () => {
pendingWithoutWasmd();
const client = new CosmWasmClient(wasmd.endpoint);
const openedClient = (client as unknown) as PrivateCosmWasmClient;
const getCodeSpy = spyOn(openedClient.restClient, "nodeInfo").and.callThrough();
const getCodeSpy = spyOn(openedClient.lcdClient, "nodeInfo").and.callThrough();
expect(await client.getChainId()).toEqual(wasmd.chainId); // from network
expect(await client.getChainId()).toEqual(wasmd.chainId); // from cache
@ -68,7 +68,7 @@ describe("CosmWasmClient", () => {
pendingWithoutWasmd();
const client = new CosmWasmClient(wasmd.endpoint);
const openedClient = (client as unknown) as PrivateCosmWasmClient;
const blockLatestSpy = spyOn(openedClient.restClient, "blocksLatest").and.callThrough();
const blockLatestSpy = spyOn(openedClient.lcdClient, "blocksLatest").and.callThrough();
const height1 = await client.getHeight();
expect(height1).toBeGreaterThan(0);
@ -85,8 +85,8 @@ describe("CosmWasmClient", () => {
const client = new CosmWasmClient(wasmd.endpoint);
const openedClient = (client as unknown) as PrivateCosmWasmClient;
const blockLatestSpy = spyOn(openedClient.restClient, "blocksLatest").and.callThrough();
const authAccountsSpy = spyOn(openedClient.restClient, "authAccounts").and.callThrough();
const blockLatestSpy = spyOn(openedClient.lcdClient, "blocksLatest").and.callThrough();
const authAccountsSpy = spyOn(openedClient.lcdClient.auth, "account").and.callThrough();
const height1 = await client.getHeight();
expect(height1).toBeGreaterThan(0);
@ -292,7 +292,7 @@ describe("CosmWasmClient", () => {
pendingWithoutWasmd();
const client = new CosmWasmClient(wasmd.endpoint);
const openedClient = (client as unknown) as PrivateCosmWasmClient;
const getCodeSpy = spyOn(openedClient.restClient, "getCode").and.callThrough();
const getCodeSpy = spyOn(openedClient.lcdClient.wasm, "getCode").and.callThrough();
const result1 = await client.getCodeDetails(deployedErc20.codeId); // from network
const result2 = await client.getCodeDetails(deployedErc20.codeId); // from cache

View File

@ -2,17 +2,20 @@ import { Sha256 } from "@cosmjs/crypto";
import { fromBase64, fromHex, toHex } from "@cosmjs/encoding";
import { Uint53 } from "@cosmjs/math";
import {
AuthExtension,
BroadcastMode,
Coin,
CosmosSdkTx,
decodeBech32Pubkey,
IndexedTx,
LcdClient,
PubKey,
setupAuthExtension,
StdTx,
} from "@cosmjs/sdk38";
import { setupWasmExtension, WasmExtension } from "./lcdapi/wasm";
import { Log, parseLogs } from "./logs";
import { RestClient } from "./restclient";
import { JsonObject } from "./types";
export interface GetNonceResult {
@ -104,7 +107,18 @@ export interface Code {
readonly creator: string;
/** Hex-encoded sha256 hash of the code stored here */
readonly checksum: string;
/**
* An URL to a .tar.gz archive of the source code of the contract, which can be used to reproducibly build the Wasm bytecode.
*
* @see https://github.com/CosmWasm/cosmwasm-verify
*/
readonly source?: string;
/**
* A docker image (including version) to reproducibly build the Wasm bytecode from the source code.
*
* @example ```cosmwasm/rust-optimizer:0.8.0```
* @see https://github.com/CosmWasm/cosmwasm-verify
*/
readonly builder?: string;
}
@ -149,11 +163,11 @@ export interface Block {
/** Use for testing only */
export interface PrivateCosmWasmClient {
readonly restClient: RestClient;
readonly lcdClient: LcdClient & AuthExtension & WasmExtension;
}
export class CosmWasmClient {
protected readonly restClient: RestClient;
protected readonly lcdClient: LcdClient & AuthExtension & WasmExtension;
/** Any address the chain considers valid (valid bech32 with proper prefix) */
protected anyValidAddress: string | undefined;
@ -170,12 +184,16 @@ export class CosmWasmClient {
* @param broadcastMode Defines at which point of the transaction processing the postTx method (i.e. transaction broadcasting) returns
*/
public constructor(apiUrl: string, broadcastMode = BroadcastMode.Block) {
this.restClient = new RestClient(apiUrl, broadcastMode);
this.lcdClient = LcdClient.withExtensions(
{ apiUrl: apiUrl, broadcastMode: broadcastMode },
setupAuthExtension,
setupWasmExtension,
);
}
public async getChainId(): Promise<string> {
if (!this.chainId) {
const response = await this.restClient.nodeInfo();
const response = await this.lcdClient.nodeInfo();
const chainId = response.node_info.network;
if (!chainId) throw new Error("Chain ID must not be empty");
this.chainId = chainId;
@ -186,12 +204,12 @@ export class CosmWasmClient {
public async getHeight(): Promise<number> {
if (this.anyValidAddress) {
const { height } = await this.restClient.authAccounts(this.anyValidAddress);
const { height } = await this.lcdClient.auth.account(this.anyValidAddress);
return parseInt(height, 10);
} else {
// Note: this gets inefficient when blocks contain a lot of transactions since it
// requires downloading and deserializing all transactions in the block.
const latest = await this.restClient.blocksLatest();
const latest = await this.lcdClient.blocksLatest();
return parseInt(latest.block.header.height, 10);
}
}
@ -201,7 +219,7 @@ export class CosmWasmClient {
*/
public async getIdentifier(tx: CosmosSdkTx): Promise<string> {
// We consult the REST API because we don't have a local amino encoder
const response = await this.restClient.encodeTx(tx);
const response = await this.lcdClient.encodeTx(tx);
const hash = new Sha256(fromBase64(response.tx)).digest();
return toHex(hash).toUpperCase();
}
@ -227,7 +245,7 @@ export class CosmWasmClient {
}
public async getAccount(address: string): Promise<Account | undefined> {
const account = await this.restClient.authAccounts(address);
const account = await this.lcdClient.auth.account(address);
const value = account.result.value;
if (value.address === "") {
return undefined;
@ -250,7 +268,7 @@ export class CosmWasmClient {
*/
public async getBlock(height?: number): Promise<Block> {
const response =
height !== undefined ? await this.restClient.blocks(height) : await this.restClient.blocksLatest();
height !== undefined ? await this.lcdClient.blocks(height) : await this.lcdClient.blocksLatest();
return {
id: response.block_id.hash,
@ -322,7 +340,7 @@ export class CosmWasmClient {
}
public async postTx(tx: StdTx): Promise<PostTxResult> {
const result = await this.restClient.postTx(tx);
const result = await this.lcdClient.postTx(tx);
if (!result.txhash.match(/^([0-9A-F][0-9A-F])+$/)) {
throw new Error("Received ill-formatted txhash. Must be non-empty upper-case hex");
}
@ -343,7 +361,7 @@ export class CosmWasmClient {
}
public async getCodes(): Promise<readonly Code[]> {
const result = await this.restClient.listCodeInfo();
const result = await this.lcdClient.wasm.listCodeInfo();
return result.map(
(entry): Code => {
this.anyValidAddress = entry.creator;
@ -362,7 +380,7 @@ export class CosmWasmClient {
const cached = this.codesCache.get(codeId);
if (cached) return cached;
const getCodeResult = await this.restClient.getCode(codeId);
const getCodeResult = await this.lcdClient.wasm.getCode(codeId);
const codeDetails: CodeDetails = {
id: getCodeResult.id,
creator: getCodeResult.creator,
@ -376,7 +394,7 @@ export class CosmWasmClient {
}
public async getContracts(codeId: number): Promise<readonly Contract[]> {
const result = await this.restClient.listContractsByCodeId(codeId);
const result = await this.lcdClient.wasm.listContractsByCodeId(codeId);
return result.map(
(entry): Contract => ({
address: entry.address,
@ -392,7 +410,7 @@ export class CosmWasmClient {
* Throws an error if no contract was found at the address
*/
public async getContract(address: string): Promise<ContractDetails> {
const result = await this.restClient.getContractInfo(address);
const result = await this.lcdClient.wasm.getContractInfo(address);
if (!result) throw new Error(`No contract found at address "${address}"`);
return {
address: result.address,
@ -414,7 +432,7 @@ export class CosmWasmClient {
// just test contract existence
const _info = await this.getContract(address);
return this.restClient.queryContractRaw(address, key);
return this.lcdClient.wasm.queryContractRaw(address, key);
}
/**
@ -426,7 +444,7 @@ export class CosmWasmClient {
*/
public async queryContractSmart(address: string, queryMsg: object): Promise<JsonObject> {
try {
return await this.restClient.queryContractSmart(address, queryMsg);
return await this.lcdClient.wasm.queryContractSmart(address, queryMsg);
} catch (error) {
if (error instanceof Error) {
if (error.message.startsWith("not found: contract")) {
@ -443,7 +461,7 @@ export class CosmWasmClient {
private async txsQuery(query: string): Promise<readonly IndexedTx[]> {
// TODO: we need proper pagination support
const limit = 100;
const result = await this.restClient.txsQuery(`${query}&limit=${limit}`);
const result = await this.lcdClient.txsQuery(`${query}&limit=${limit}`);
const pages = parseInt(result.page_total, 10);
if (pages > 1) {
throw new Error(

View File

@ -1,7 +1,7 @@
import * as logs from "./logs";
export { logs };
export { RestClient } from "./restclient";
export { setupWasmExtension, WasmExtension } from "./lcdapi/wasm";
export {
Account,
Block,

View File

@ -2,29 +2,28 @@
import { Sha256 } from "@cosmjs/crypto";
import { Bech32, fromAscii, fromBase64, fromHex, toAscii, toBase64, toHex } from "@cosmjs/encoding";
import {
AuthExtension,
Coin,
coin,
coins,
LcdClient,
makeSignBytes,
Msg,
Pen,
PostTxsResponse,
Secp256k1Pen,
setupAuthExtension,
StdFee,
StdSignature,
StdTx,
} from "@cosmjs/sdk38";
import { assert } from "@cosmjs/utils";
import { findAttribute, parseLogs } from "./logs";
import { findAttribute, parseLogs } from "../logs";
import {
isMsgInstantiateContract,
isMsgStoreCode,
MsgExecuteContract,
MsgInstantiateContract,
MsgStoreCode,
} from "./msgs";
import { RestClient } from "./restclient";
} from "../msgs";
import {
alice,
bech32AddressMatcher,
@ -33,22 +32,21 @@ import {
fromOneElementArray,
getHackatom,
makeRandomAddress,
makeSignedTx,
pendingWithoutWasmd,
wasmd,
wasmdEnabled,
} from "./testutils.spec";
} from "../testutils.spec";
import { setupWasmExtension, WasmExtension } from "./wasm";
function makeSignedTx(firstMsg: Msg, fee: StdFee, memo: string, firstSignature: StdSignature): StdTx {
return {
msg: [firstMsg],
fee: fee,
memo: memo,
signatures: [firstSignature],
};
type WasmClient = LcdClient & AuthExtension & WasmExtension;
function makeWasmClient(apiUrl: string): WasmClient {
return LcdClient.withExtensions({ apiUrl }, setupAuthExtension, setupWasmExtension);
}
async function uploadContract(
client: RestClient,
client: WasmClient,
pen: Pen,
contract: ContractUploadInstructions,
): Promise<PostTxsResponse> {
@ -72,7 +70,7 @@ async function uploadContract(
gas: "89000000",
};
const { account_number, sequence } = (await client.authAccounts(alice.address0)).result.value;
const { account_number, sequence } = (await client.auth.account(alice.address0)).result.value;
const signBytes = makeSignBytes([theMsg], fee, wasmd.chainId, memo, account_number, sequence);
const signature = await pen.sign(signBytes);
const signedTx = makeSignedTx(theMsg, fee, memo, signature);
@ -80,7 +78,7 @@ async function uploadContract(
}
async function instantiateContract(
client: RestClient,
client: WasmClient,
pen: Pen,
codeId: number,
beneficiaryAddress: string,
@ -110,7 +108,7 @@ async function instantiateContract(
gas: "89000000",
};
const { account_number, sequence } = (await client.authAccounts(alice.address0)).result.value;
const { account_number, sequence } = (await client.auth.account(alice.address0)).result.value;
const signBytes = makeSignBytes([theMsg], fee, wasmd.chainId, memo, account_number, sequence);
const signature = await pen.sign(signBytes);
const signedTx = makeSignedTx(theMsg, fee, memo, signature);
@ -118,7 +116,7 @@ async function instantiateContract(
}
async function executeContract(
client: RestClient,
client: WasmClient,
pen: Pen,
contractAddress: string,
msg: object,
@ -138,23 +136,23 @@ async function executeContract(
gas: "89000000",
};
const { account_number, sequence } = (await client.authAccounts(alice.address0)).result.value;
const { account_number, sequence } = (await client.auth.account(alice.address0)).result.value;
const signBytes = makeSignBytes([theMsg], fee, wasmd.chainId, memo, account_number, sequence);
const signature = await pen.sign(signBytes);
const signedTx = makeSignedTx(theMsg, fee, memo, signature);
return client.postTx(signedTx);
}
describe("RestClient", () => {
describe("wasm", () => {
it("can be constructed", () => {
const client = new RestClient(wasmd.endpoint);
const client = makeWasmClient(wasmd.endpoint);
expect(client).toBeTruthy();
});
describe("txsQuery", () => {
it("can query by tags (module + code_id)", async () => {
pendingWithoutWasmd();
const client = new RestClient(wasmd.endpoint);
const client = makeWasmClient(wasmd.endpoint);
const result = await client.txsQuery(`message.module=wasm&message.code_id=${deployedErc20.codeId}`);
expect(parseInt(result.count, 10)).toBeGreaterThanOrEqual(4);
@ -200,7 +198,7 @@ describe("RestClient", () => {
// Like previous test but filtered by message.action=store-code and message.action=instantiate
it("can query by tags (module + code_id + action)", async () => {
pendingWithoutWasmd();
const client = new RestClient(wasmd.endpoint);
const client = makeWasmClient(wasmd.endpoint);
{
const uploads = await client.txsQuery(
@ -259,7 +257,7 @@ describe("RestClient", () => {
it("can upload, instantiate and execute wasm", async () => {
pendingWithoutWasmd();
const pen = await Secp256k1Pen.fromMnemonic(alice.mnemonic);
const client = new RestClient(wasmd.endpoint);
const client = makeWasmClient(wasmd.endpoint);
const transferAmount = [coin(1234, "ucosm"), coin(321, "ustake")];
const beneficiaryAddress = makeRandomAddress();
@ -293,7 +291,7 @@ describe("RestClient", () => {
expect(amountAttr.value).toEqual("1234ucosm,321ustake");
expect(result.data).toEqual(toHex(Bech32.decode(contractAddress).data).toUpperCase());
const balance = (await client.authAccounts(contractAddress)).result.value.coins;
const balance = (await client.auth.account(contractAddress)).result.value.coins;
expect(balance).toEqual(transferAmount);
}
@ -313,9 +311,9 @@ describe("RestClient", () => {
});
// Verify token transfer from contract to beneficiary
const beneficiaryBalance = (await client.authAccounts(beneficiaryAddress)).result.value.coins;
const beneficiaryBalance = (await client.auth.account(beneficiaryAddress)).result.value.coins;
expect(beneficiaryBalance).toEqual(transferAmount);
const contractBalance = (await client.authAccounts(contractAddress)).result.value.coins;
const contractBalance = (await client.auth.account(contractAddress)).result.value.coins;
expect(contractBalance).toEqual([]);
}
});
@ -327,10 +325,10 @@ describe("RestClient", () => {
it("can list upload code", async () => {
pendingWithoutWasmd();
const pen = await Secp256k1Pen.fromMnemonic(alice.mnemonic);
const client = new RestClient(wasmd.endpoint);
const client = makeWasmClient(wasmd.endpoint);
// check with contracts were here first to compare
const existingInfos = await client.listCodeInfo();
const existingInfos = await client.wasm.listCodeInfo();
existingInfos.forEach((val, idx) => expect(val.id).toEqual(idx + 1));
const numExisting = existingInfos.length;
@ -343,7 +341,7 @@ describe("RestClient", () => {
const codeId = Number.parseInt(codeIdAttr.value, 10);
// ensure we were added to the end of the list
const newInfos = await client.listCodeInfo();
const newInfos = await client.wasm.listCodeInfo();
expect(newInfos.length).toEqual(numExisting + 1);
const lastInfo = newInfos[newInfos.length - 1];
expect(lastInfo.id).toEqual(codeId);
@ -358,14 +356,14 @@ describe("RestClient", () => {
expect(lastInfo.data_hash.toLowerCase()).toEqual(toHex(wasmHash));
// download code and check against auto-gen
const { data } = await client.getCode(codeId);
const { data } = await client.wasm.getCode(codeId);
expect(fromBase64(data)).toEqual(hackatom.data);
});
it("can list contracts and get info", async () => {
pendingWithoutWasmd();
const pen = await Secp256k1Pen.fromMnemonic(alice.mnemonic);
const client = new RestClient(wasmd.endpoint);
const client = makeWasmClient(wasmd.endpoint);
const beneficiaryAddress = makeRandomAddress();
const transferAmount: readonly Coin[] = [
{
@ -376,7 +374,7 @@ describe("RestClient", () => {
// reuse an existing contract, or upload if needed
let codeId: number;
const existingInfos = await client.listCodeInfo();
const existingInfos = await client.wasm.listCodeInfo();
if (existingInfos.length > 0) {
codeId = existingInfos[existingInfos.length - 1].id;
} else {
@ -388,7 +386,7 @@ describe("RestClient", () => {
}
// create new instance and compare before and after
const existingContractsByCode = await client.listContractsByCodeId(codeId);
const existingContractsByCode = await client.wasm.listContractsByCodeId(codeId);
for (const contract of existingContractsByCode) {
expect(contract.address).toMatch(bech32AddressMatcher);
expect(contract.code_id).toEqual(codeId);
@ -402,7 +400,7 @@ describe("RestClient", () => {
const contractAddressAttr = findAttribute(logs, "message", "contract_address");
const myAddress = contractAddressAttr.value;
const newContractsByCode = await client.listContractsByCodeId(codeId);
const newContractsByCode = await client.wasm.listContractsByCodeId(codeId);
expect(newContractsByCode.length).toEqual(existingContractsByCode.length + 1);
const newContract = newContractsByCode[newContractsByCode.length - 1];
expect(newContract).toEqual(
@ -414,7 +412,7 @@ describe("RestClient", () => {
);
// check out info
const myInfo = await client.getContractInfo(myAddress);
const myInfo = await client.wasm.getContractInfo(myAddress);
assert(myInfo);
expect(myInfo).toEqual(
jasmine.objectContaining({
@ -429,11 +427,11 @@ describe("RestClient", () => {
// make sure random addresses don't give useful info
const nonExistentAddress = makeRandomAddress();
expect(await client.getContractInfo(nonExistentAddress)).toBeNull();
expect(await client.wasm.getContractInfo(nonExistentAddress)).toBeNull();
});
describe("contract state", () => {
const client = new RestClient(wasmd.endpoint);
const client = makeWasmClient(wasmd.endpoint);
const noContract = makeRandomAddress();
const expectedKey = toAscii("config");
let contractAddress: string | undefined;
@ -457,7 +455,7 @@ describe("RestClient", () => {
pendingWithoutWasmd();
// get contract state
const state = await client.getAllContractState(contractAddress!);
const state = await client.wasm.getAllContractState(contractAddress!);
expect(state.length).toEqual(1);
const data = state[0];
expect(data.key).toEqual(expectedKey);
@ -466,7 +464,7 @@ describe("RestClient", () => {
expect(value.beneficiary).toBeDefined();
// bad address is empty array
const noContractState = await client.getAllContractState(noContract);
const noContractState = await client.wasm.getAllContractState(noContract);
expect(noContractState).toEqual([]);
});
@ -474,18 +472,18 @@ describe("RestClient", () => {
pendingWithoutWasmd();
// query by one key
const raw = await client.queryContractRaw(contractAddress!, expectedKey);
const raw = await client.wasm.queryContractRaw(contractAddress!, expectedKey);
assert(raw, "must get result");
const model = JSON.parse(fromAscii(raw));
expect(model.verifier).toBeDefined();
expect(model.beneficiary).toBeDefined();
// missing key is null
const missing = await client.queryContractRaw(contractAddress!, fromHex("cafe0dad"));
const missing = await client.wasm.queryContractRaw(contractAddress!, fromHex("cafe0dad"));
expect(missing).toBeNull();
// bad address is null
const noContractModel = await client.queryContractRaw(noContract, expectedKey);
const noContractModel = await client.wasm.queryContractRaw(noContract, expectedKey);
expect(noContractModel).toBeNull();
});
@ -493,18 +491,18 @@ describe("RestClient", () => {
pendingWithoutWasmd();
// we can query the verifier properly
const resultDocument = await client.queryContractSmart(contractAddress!, { verifier: {} });
const resultDocument = await client.wasm.queryContractSmart(contractAddress!, { verifier: {} });
expect(resultDocument).toEqual({ verifier: alice.address0 });
// invalid query syntax throws an error
await client.queryContractSmart(contractAddress!, { nosuchkey: {} }).then(
await client.wasm.queryContractSmart(contractAddress!, { nosuchkey: {} }).then(
() => fail("shouldn't succeed"),
(error) =>
expect(error).toMatch(/query wasm contract failed: parsing hackatom::contract::QueryMsg/),
);
// invalid address throws an error
await client.queryContractSmart(noContract, { verifier: {} }).then(
await client.wasm.queryContractSmart(noContract, { verifier: {} }).then(
() => fail("shouldn't succeed"),
(error) => expect(error).toMatch("not found"),
);

View File

@ -0,0 +1,161 @@
import { fromBase64, fromUtf8, toHex, toUtf8 } from "@cosmjs/encoding";
import { LcdApiArray, LcdClient, normalizeLcdApiArray } from "@cosmjs/sdk38";
import { JsonObject, Model, parseWasmData, WasmData } from "../types";
type WasmResponse<T> = WasmSuccess<T> | WasmError;
interface WasmSuccess<T> {
readonly height: string;
readonly result: T;
}
interface WasmError {
readonly error: string;
}
export interface CodeInfo {
readonly id: number;
/** Bech32 account address */
readonly creator: string;
/** Hex-encoded sha256 hash of the code stored here */
readonly data_hash: string;
/**
* An URL to a .tar.gz archive of the source code of the contract, which can be used to reproducibly build the Wasm bytecode.
*
* @see https://github.com/CosmWasm/cosmwasm-verify
*/
readonly source?: string;
/**
* A docker image (including version) to reproducibly build the Wasm bytecode from the source code.
*
* @example ```cosmwasm/rust-optimizer:0.8.0```
* @see https://github.com/CosmWasm/cosmwasm-verify
*/
readonly builder?: string;
}
export interface CodeDetails extends CodeInfo {
/** Base64 encoded raw wasm data */
readonly data: string;
}
// This is list view, without contract info
export interface ContractInfo {
readonly address: string;
readonly code_id: number;
/** Bech32 account address */
readonly creator: string;
/** Bech32-encoded admin address */
readonly admin?: string;
readonly label: string;
}
export interface ContractDetails extends ContractInfo {
/** Argument passed on initialization of the contract */
readonly init_msg: object;
}
interface SmartQueryResponse {
// base64 encoded response
readonly smart: string;
}
function isWasmError<T>(resp: WasmResponse<T>): resp is WasmError {
return (resp as WasmError).error !== undefined;
}
function unwrapWasmResponse<T>(response: WasmResponse<T>): T {
if (isWasmError(response)) {
throw new Error(response.error);
}
return response.result;
}
/**
* @see https://github.com/cosmwasm/wasmd/blob/master/x/wasm/client/rest/query.go#L19-L27
*/
export interface WasmExtension {
readonly wasm: {
readonly listCodeInfo: () => Promise<readonly CodeInfo[]>;
/**
* Downloads the original wasm bytecode by code ID.
*
* Throws an error if no code with this id
*/
readonly getCode: (id: number) => Promise<CodeDetails>;
readonly listContractsByCodeId: (id: number) => Promise<readonly ContractInfo[]>;
/**
* Returns null when contract was not found at this address.
*/
readonly getContractInfo: (address: string) => Promise<ContractDetails | null>;
/**
* Returns all contract state.
* This is an empty array if no such contract, or contract has no data.
*/
readonly getAllContractState: (address: string) => Promise<readonly Model[]>;
/**
* Returns the data at the key if present (unknown decoded json),
* or null if no data at this (contract address, key) pair
*/
readonly queryContractRaw: (address: string, key: Uint8Array) => Promise<Uint8Array | null>;
/**
* Makes a smart query on the contract and parses the response as JSON.
* Throws error if no such contract exists, the query format is invalid or the response is invalid.
*/
readonly queryContractSmart: (address: string, query: object) => Promise<JsonObject>;
};
}
export function setupWasmExtension(base: LcdClient): WasmExtension {
return {
wasm: {
listCodeInfo: async () => {
const path = `/wasm/code`;
const responseData = (await base.get(path)) as WasmResponse<LcdApiArray<CodeInfo>>;
return normalizeLcdApiArray(unwrapWasmResponse(responseData));
},
getCode: async (id: number) => {
const path = `/wasm/code/${id}`;
const responseData = (await base.get(path)) as WasmResponse<CodeDetails>;
return unwrapWasmResponse(responseData);
},
listContractsByCodeId: async (id: number) => {
const path = `/wasm/code/${id}/contracts`;
const responseData = (await base.get(path)) as WasmResponse<LcdApiArray<ContractInfo>>;
return normalizeLcdApiArray(unwrapWasmResponse(responseData));
},
getContractInfo: async (address: string) => {
const path = `/wasm/contract/${address}`;
const response = (await base.get(path)) as WasmResponse<ContractDetails | null>;
return unwrapWasmResponse(response);
},
getAllContractState: async (address: string) => {
const path = `/wasm/contract/${address}/state`;
const responseData = (await base.get(path)) as WasmResponse<LcdApiArray<WasmData>>;
return normalizeLcdApiArray(unwrapWasmResponse(responseData)).map(parseWasmData);
},
queryContractRaw: async (address: string, key: Uint8Array) => {
const hexKey = toHex(key);
const path = `/wasm/contract/${address}/raw/${hexKey}?encoding=hex`;
const responseData = (await base.get(path)) as WasmResponse<WasmData[]>;
const data = unwrapWasmResponse(responseData);
return data.length === 0 ? null : fromBase64(data[0].val);
},
queryContractSmart: async (address: string, query: object) => {
const encoded = toHex(toUtf8(JSON.stringify(query)));
const path = `/wasm/contract/${address}/smart/${encoded}?encoding=hex`;
const responseData = (await base.get(path)) as WasmResponse<SmartQueryResponse>;
const result = unwrapWasmResponse(responseData);
// By convention, smart queries must return a valid JSON document (see https://github.com/CosmWasm/cosmwasm/issues/144)
return JSON.parse(fromUtf8(fromBase64(result.smart)));
},
},
};
}

View File

@ -1,153 +0,0 @@
import { fromBase64, fromUtf8, toHex, toUtf8 } from "@cosmjs/encoding";
import { BroadcastMode, RestClient as BaseRestClient } from "@cosmjs/sdk38";
import { JsonObject, Model, parseWasmData, WasmData } from "./types";
// Currently all wasm query responses return json-encoded strings...
// later deprecate this and use the specific types for result
// (assuming it is inlined, no second parse needed)
type WasmResponse<T = string> = WasmSuccess<T> | WasmError;
interface WasmSuccess<T = string> {
readonly height: string;
readonly result: T;
}
interface WasmError {
readonly error: string;
}
export interface CodeInfo {
readonly id: number;
/** Bech32 account address */
readonly creator: string;
/** Hex-encoded sha256 hash of the code stored here */
readonly data_hash: string;
// TODO: these are not supported in current wasmd
readonly source?: string;
readonly builder?: string;
}
export interface CodeDetails extends CodeInfo {
/** Base64 encoded raw wasm data */
readonly data: string;
}
// This is list view, without contract info
export interface ContractInfo {
readonly address: string;
readonly code_id: number;
/** Bech32 account address */
readonly creator: string;
/** Bech32-encoded admin address */
readonly admin?: string;
readonly label: string;
}
export interface ContractDetails extends ContractInfo {
/** Argument passed on initialization of the contract */
readonly init_msg: object;
}
interface SmartQueryResponse {
// base64 encoded response
readonly smart: string;
}
/** Unfortunately, Cosmos SDK encodes empty arrays as null */
type CosmosSdkArray<T> = readonly T[] | null;
function normalizeArray<T>(backend: CosmosSdkArray<T>): readonly T[] {
return backend || [];
}
function isWasmError<T>(resp: WasmResponse<T>): resp is WasmError {
return (resp as WasmError).error !== undefined;
}
function unwrapWasmResponse<T>(response: WasmResponse<T>): T {
if (isWasmError(response)) {
throw new Error(response.error);
}
return response.result;
}
export class RestClient extends BaseRestClient {
/**
* Creates a new client to interact with a Cosmos SDK light client daemon.
* This class tries to be a direct mapping onto the API. Some basic decoding and normalizatin is done
* but things like caching are done at a higher level.
*
* When building apps, you should not need to use this class directly. If you do, this indicates a missing feature
* in higher level components. Feel free to raise an issue in this case.
*
* @param apiUrl The URL of a Cosmos SDK light client daemon API (sometimes called REST server or REST API)
* @param broadcastMode Defines at which point of the transaction processing the postTx method (i.e. transaction broadcasting) returns
*/
public constructor(apiUrl: string, broadcastMode = BroadcastMode.Block) {
super(apiUrl, broadcastMode);
}
// The /wasm endpoints
// wasm rest queries are listed here: https://github.com/cosmwasm/wasmd/blob/master/x/wasm/client/rest/query.go#L19-L27
public async listCodeInfo(): Promise<readonly CodeInfo[]> {
const path = `/wasm/code`;
const responseData = (await this.get(path)) as WasmResponse<CosmosSdkArray<CodeInfo>>;
return normalizeArray(unwrapWasmResponse(responseData));
}
// this will download the original wasm bytecode by code id
// throws error if no code with this id
public async getCode(id: number): Promise<CodeDetails> {
const path = `/wasm/code/${id}`;
const responseData = (await this.get(path)) as WasmResponse<CodeDetails>;
return unwrapWasmResponse(responseData);
}
public async listContractsByCodeId(id: number): Promise<readonly ContractInfo[]> {
const path = `/wasm/code/${id}/contracts`;
const responseData = (await this.get(path)) as WasmResponse<CosmosSdkArray<ContractInfo>>;
return normalizeArray(unwrapWasmResponse(responseData));
}
/**
* Returns null when contract was not found at this address.
*/
public async getContractInfo(address: string): Promise<ContractDetails | null> {
const path = `/wasm/contract/${address}`;
const response = (await this.get(path)) as WasmResponse<ContractDetails | null>;
return unwrapWasmResponse(response);
}
// Returns all contract state.
// This is an empty array if no such contract, or contract has no data.
public async getAllContractState(address: string): Promise<readonly Model[]> {
const path = `/wasm/contract/${address}/state`;
const responseData = (await this.get(path)) as WasmResponse<CosmosSdkArray<WasmData>>;
return normalizeArray(unwrapWasmResponse(responseData)).map(parseWasmData);
}
// Returns the data at the key if present (unknown decoded json),
// or null if no data at this (contract address, key) pair
public async queryContractRaw(address: string, key: Uint8Array): Promise<Uint8Array | null> {
const hexKey = toHex(key);
const path = `/wasm/contract/${address}/raw/${hexKey}?encoding=hex`;
const responseData = (await this.get(path)) as WasmResponse<WasmData[]>;
const data = unwrapWasmResponse(responseData);
return data.length === 0 ? null : fromBase64(data[0].val);
}
/**
* Makes a smart query on the contract and parses the reponse as JSON.
* Throws error if no such contract exists, the query format is invalid or the response is invalid.
*/
public async queryContractSmart(address: string, query: object): Promise<JsonObject> {
const encoded = toHex(toUtf8(JSON.stringify(query)));
const path = `/wasm/contract/${address}/smart/${encoded}?encoding=hex`;
const responseData = (await this.get(path)) as WasmResponse<SmartQueryResponse>;
const result = unwrapWasmResponse(responseData);
// By convention, smart queries must return a valid JSON document (see https://github.com/CosmWasm/cosmwasm/issues/144)
return JSON.parse(fromUtf8(fromBase64(result.smart)));
}
}

View File

@ -1,15 +1,19 @@
import { Sha256 } from "@cosmjs/crypto";
import { toHex } from "@cosmjs/encoding";
import { coin, coins, Secp256k1Pen } from "@cosmjs/sdk38";
import { AuthExtension, coin, coins, LcdClient, Secp256k1Pen, setupAuthExtension } from "@cosmjs/sdk38";
import { assert } from "@cosmjs/utils";
import { isPostTxFailure, PrivateCosmWasmClient } from "./cosmwasmclient";
import { RestClient } from "./restclient";
import { setupWasmExtension, WasmExtension } from "./lcdapi/wasm";
import { SigningCosmWasmClient, UploadMeta } from "./signingcosmwasmclient";
import { alice, getHackatom, makeRandomAddress, pendingWithoutWasmd, unused } from "./testutils.spec";
const httpUrl = "http://localhost:1317";
function makeWasmClient(apiUrl: string): LcdClient & AuthExtension & WasmExtension {
return LcdClient.withExtensions({ apiUrl }, setupAuthExtension, setupWasmExtension);
}
describe("SigningCosmWasmClient", () => {
describe("makeReadOnly", () => {
it("can be constructed", async () => {
@ -26,8 +30,8 @@ describe("SigningCosmWasmClient", () => {
const client = new SigningCosmWasmClient(httpUrl, alice.address0, (signBytes) => pen.sign(signBytes));
const openedClient = (client as unknown) as PrivateCosmWasmClient;
const blockLatestSpy = spyOn(openedClient.restClient, "blocksLatest").and.callThrough();
const authAccountsSpy = spyOn(openedClient.restClient, "authAccounts").and.callThrough();
const blockLatestSpy = spyOn(openedClient.lcdClient, "blocksLatest").and.callThrough();
const authAccountsSpy = spyOn(openedClient.lcdClient.auth, "account").and.callThrough();
const height = await client.getHeight();
expect(height).toBeGreaterThan(0);
@ -97,8 +101,8 @@ describe("SigningCosmWasmClient", () => {
},
);
const rest = new RestClient(httpUrl);
const balance = (await rest.authAccounts(contractAddress)).result.value.coins;
const lcdClient = makeWasmClient(httpUrl);
const balance = (await lcdClient.auth.account(contractAddress)).result.value.coins;
expect(balance).toEqual(transferAmount);
});
@ -119,8 +123,8 @@ describe("SigningCosmWasmClient", () => {
{ admin: unused.address },
);
const rest = new RestClient(httpUrl);
const contract = await rest.getContractInfo(contractAddress);
const lcdClient = makeWasmClient(httpUrl);
const contract = await lcdClient.wasm.getContractInfo(contractAddress);
assert(contract);
expect(contract.admin).toEqual(unused.address);
});
@ -171,14 +175,14 @@ describe("SigningCosmWasmClient", () => {
},
);
const rest = new RestClient(httpUrl);
const state1 = await rest.getContractInfo(contractAddress);
const lcdClient = makeWasmClient(httpUrl);
const state1 = await lcdClient.wasm.getContractInfo(contractAddress);
assert(state1);
expect(state1.admin).toEqual(alice.address0);
await client.updateAdmin(contractAddress, unused.address);
const state2 = await rest.getContractInfo(contractAddress);
const state2 = await lcdClient.wasm.getContractInfo(contractAddress);
assert(state2);
expect(state2.admin).toEqual(unused.address);
});
@ -204,14 +208,14 @@ describe("SigningCosmWasmClient", () => {
},
);
const rest = new RestClient(httpUrl);
const state1 = await rest.getContractInfo(contractAddress);
const lcdClient = makeWasmClient(httpUrl);
const state1 = await lcdClient.wasm.getContractInfo(contractAddress);
assert(state1);
expect(state1.admin).toEqual(alice.address0);
await client.clearAdmin(contractAddress);
const state2 = await rest.getContractInfo(contractAddress);
const state2 = await lcdClient.wasm.getContractInfo(contractAddress);
assert(state2);
expect(state2.admin).toBeUndefined();
});
@ -238,15 +242,15 @@ describe("SigningCosmWasmClient", () => {
},
);
const rest = new RestClient(httpUrl);
const state1 = await rest.getContractInfo(contractAddress);
const lcdClient = makeWasmClient(httpUrl);
const state1 = await lcdClient.wasm.getContractInfo(contractAddress);
assert(state1);
expect(state1.admin).toEqual(alice.address0);
const newVerifier = makeRandomAddress();
await client.migrate(contractAddress, codeId2, { verifier: newVerifier });
const state2 = await rest.getContractInfo(contractAddress);
const state2 = await lcdClient.wasm.getContractInfo(contractAddress);
assert(state2);
expect(state2).toEqual({
...state1,
@ -289,10 +293,10 @@ describe("SigningCosmWasmClient", () => {
});
// Verify token transfer from contract to beneficiary
const rest = new RestClient(httpUrl);
const beneficiaryBalance = (await rest.authAccounts(beneficiaryAddress)).result.value.coins;
const lcdClient = makeWasmClient(httpUrl);
const beneficiaryBalance = (await lcdClient.auth.account(beneficiaryAddress)).result.value.coins;
expect(beneficiaryBalance).toEqual(transferAmount);
const contractBalance = (await rest.authAccounts(contractAddress)).result.value.coins;
const contractBalance = (await lcdClient.auth.account(contractAddress)).result.value.coins;
expect(contractBalance).toEqual([]);
});
});

View File

@ -83,9 +83,18 @@ const defaultFees: FeeTable = {
};
export interface UploadMeta {
/** The source URL */
/**
* An URL to a .tar.gz archive of the source code of the contract, which can be used to reproducibly build the Wasm bytecode.
*
* @see https://github.com/CosmWasm/cosmwasm-verify
*/
readonly source?: string;
/** The builder tag */
/**
* A docker image (including version) to reproducibly build the Wasm bytecode from the source code.
*
* @example ```cosmwasm/rust-optimizer:0.8.0```
* @see https://github.com/CosmWasm/cosmwasm-verify
*/
readonly builder?: string;
}

View File

@ -1,5 +1,6 @@
import { Random } from "@cosmjs/crypto";
import { Bech32, fromBase64 } from "@cosmjs/encoding";
import { Msg, StdFee, StdSignature, StdTx } from "@cosmjs/sdk38";
import hackatom from "./testdata/contract.json";
@ -85,3 +86,12 @@ export function fromOneElementArray<T>(elements: ArrayLike<T>): T {
if (elements.length !== 1) throw new Error(`Expected exactly one element but got ${elements.length}`);
return elements[0];
}
export function makeSignedTx(firstMsg: Msg, fee: StdFee, memo: string, firstSignature: StdSignature): StdTx {
return {
msg: [firstMsg],
fee: fee,
memo: memo,
signatures: [firstSignature],
};
}

View File

@ -1,6 +1,15 @@
import { BroadcastMode, Coin, CosmosSdkTx, IndexedTx, PubKey, StdTx } from "@cosmjs/sdk38";
import {
AuthExtension,
BroadcastMode,
Coin,
CosmosSdkTx,
IndexedTx,
LcdClient,
PubKey,
StdTx,
} from "@cosmjs/sdk38";
import { WasmExtension } from "./lcdapi/wasm";
import { Log } from "./logs";
import { RestClient } from "./restclient";
import { JsonObject } from "./types";
export interface GetNonceResult {
readonly accountNumber: number;
@ -64,7 +73,18 @@ export interface Code {
readonly creator: string;
/** Hex-encoded sha256 hash of the code stored here */
readonly checksum: string;
/**
* An URL to a .tar.gz archive of the source code of the contract, which can be used to reproducibly build the Wasm bytecode.
*
* @see https://github.com/CosmWasm/cosmwasm-verify
*/
readonly source?: string;
/**
* A docker image (including version) to reproducibly build the Wasm bytecode from the source code.
*
* @example ```cosmwasm/rust-optimizer:0.8.0```
* @see https://github.com/CosmWasm/cosmwasm-verify
*/
readonly builder?: string;
}
export interface CodeDetails extends Code {
@ -103,10 +123,10 @@ export interface Block {
}
/** Use for testing only */
export interface PrivateCosmWasmClient {
readonly restClient: RestClient;
readonly lcdClient: LcdClient & AuthExtension & WasmExtension;
}
export declare class CosmWasmClient {
protected readonly restClient: RestClient;
protected readonly lcdClient: LcdClient & AuthExtension & WasmExtension;
/** Any address the chain considers valid (valid bech32 with proper prefix) */
protected anyValidAddress: string | undefined;
private readonly codesCache;

View File

@ -1,6 +1,6 @@
import * as logs from "./logs";
export { logs };
export { RestClient } from "./restclient";
export { setupWasmExtension, WasmExtension } from "./lcdapi/wasm";
export {
Account,
Block,

View File

@ -0,0 +1,74 @@
import { LcdClient } from "@cosmjs/sdk38";
import { JsonObject, Model } from "../types";
export interface CodeInfo {
readonly id: number;
/** Bech32 account address */
readonly creator: string;
/** Hex-encoded sha256 hash of the code stored here */
readonly data_hash: string;
/**
* An URL to a .tar.gz archive of the source code of the contract, which can be used to reproducibly build the Wasm bytecode.
*
* @see https://github.com/CosmWasm/cosmwasm-verify
*/
readonly source?: string;
/**
* A docker image (including version) to reproducibly build the Wasm bytecode from the source code.
*
* @example ```cosmwasm/rust-optimizer:0.8.0```
* @see https://github.com/CosmWasm/cosmwasm-verify
*/
readonly builder?: string;
}
export interface CodeDetails extends CodeInfo {
/** Base64 encoded raw wasm data */
readonly data: string;
}
export interface ContractInfo {
readonly address: string;
readonly code_id: number;
/** Bech32 account address */
readonly creator: string;
/** Bech32-encoded admin address */
readonly admin?: string;
readonly label: string;
}
export interface ContractDetails extends ContractInfo {
/** Argument passed on initialization of the contract */
readonly init_msg: object;
}
/**
* @see https://github.com/cosmwasm/wasmd/blob/master/x/wasm/client/rest/query.go#L19-L27
*/
export interface WasmExtension {
readonly wasm: {
readonly listCodeInfo: () => Promise<readonly CodeInfo[]>;
/**
* Downloads the original wasm bytecode by code ID.
*
* Throws an error if no code with this id
*/
readonly getCode: (id: number) => Promise<CodeDetails>;
readonly listContractsByCodeId: (id: number) => Promise<readonly ContractInfo[]>;
/**
* Returns null when contract was not found at this address.
*/
readonly getContractInfo: (address: string) => Promise<ContractDetails | null>;
/**
* Returns all contract state.
* This is an empty array if no such contract, or contract has no data.
*/
readonly getAllContractState: (address: string) => Promise<readonly Model[]>;
/**
* Returns the data at the key if present (unknown decoded json),
* or null if no data at this (contract address, key) pair
*/
readonly queryContractRaw: (address: string, key: Uint8Array) => Promise<Uint8Array | null>;
/**
* Makes a smart query on the contract and parses the response as JSON.
* Throws error if no such contract exists, the query format is invalid or the response is invalid.
*/
readonly queryContractSmart: (address: string, query: object) => Promise<JsonObject>;
};
}
export declare function setupWasmExtension(base: LcdClient): WasmExtension;

View File

@ -1,56 +0,0 @@
import { BroadcastMode, RestClient as BaseRestClient } from "@cosmjs/sdk38";
import { JsonObject, Model } from "./types";
export interface CodeInfo {
readonly id: number;
/** Bech32 account address */
readonly creator: string;
/** Hex-encoded sha256 hash of the code stored here */
readonly data_hash: string;
readonly source?: string;
readonly builder?: string;
}
export interface CodeDetails extends CodeInfo {
/** Base64 encoded raw wasm data */
readonly data: string;
}
export interface ContractInfo {
readonly address: string;
readonly code_id: number;
/** Bech32 account address */
readonly creator: string;
/** Bech32-encoded admin address */
readonly admin?: string;
readonly label: string;
}
export interface ContractDetails extends ContractInfo {
/** Argument passed on initialization of the contract */
readonly init_msg: object;
}
export declare class RestClient extends BaseRestClient {
/**
* Creates a new client to interact with a Cosmos SDK light client daemon.
* This class tries to be a direct mapping onto the API. Some basic decoding and normalizatin is done
* but things like caching are done at a higher level.
*
* When building apps, you should not need to use this class directly. If you do, this indicates a missing feature
* in higher level components. Feel free to raise an issue in this case.
*
* @param apiUrl The URL of a Cosmos SDK light client daemon API (sometimes called REST server or REST API)
* @param broadcastMode Defines at which point of the transaction processing the postTx method (i.e. transaction broadcasting) returns
*/
constructor(apiUrl: string, broadcastMode?: BroadcastMode);
listCodeInfo(): Promise<readonly CodeInfo[]>;
getCode(id: number): Promise<CodeDetails>;
listContractsByCodeId(id: number): Promise<readonly ContractInfo[]>;
/**
* Returns null when contract was not found at this address.
*/
getContractInfo(address: string): Promise<ContractDetails | null>;
getAllContractState(address: string): Promise<readonly Model[]>;
queryContractRaw(address: string, key: Uint8Array): Promise<Uint8Array | null>;
/**
* Makes a smart query on the contract and parses the reponse as JSON.
* Throws error if no such contract exists, the query format is invalid or the response is invalid.
*/
queryContractSmart(address: string, query: object): Promise<JsonObject>;
}

View File

@ -14,9 +14,18 @@ export interface FeeTable {
readonly changeAdmin: StdFee;
}
export interface UploadMeta {
/** The source URL */
/**
* An URL to a .tar.gz archive of the source code of the contract, which can be used to reproducibly build the Wasm bytecode.
*
* @see https://github.com/CosmWasm/cosmwasm-verify
*/
readonly source?: string;
/** The builder tag */
/**
* A docker image (including version) to reproducibly build the Wasm bytecode from the source code.
*
* @example ```cosmwasm/rust-optimizer:0.8.0```
* @see https://github.com/CosmWasm/cosmwasm-verify
*/
readonly builder?: string;
}
export interface UploadResult {

View File

@ -26,7 +26,7 @@
"format-text": "prettier --write --prose-wrap always --print-width 80 \"./*.md\"",
"lint": "eslint --max-warnings 0 \"**/*.{js,ts}\"",
"lint-fix": "eslint --max-warnings 0 \"**/*.{js,ts}\" --fix",
"move-types": "shx rm -rf ./types/* && shx mv build/types/* ./types && rm -rf ./types/testdata && shx rm -f ./types/*.spec.d.ts",
"move-types": "shx rm -rf ./types/* && shx mv build/types/* ./types && rm -rf ./types/testdata && shx rm -f ./types/*.spec.d.ts && shx rm ./types/**/*.spec.d.ts",
"format-types": "prettier --write --loglevel warn \"./types/**/*.d.ts\"",
"build": "shx rm -rf ./build && tsc && yarn move-types && yarn format-types",
"build-or-skip": "[ -n \"$SKIP_BUILD\" ] || yarn build",

View File

@ -4,9 +4,9 @@ import { assert, sleep } from "@cosmjs/utils";
import { coins } from "./coins";
import { CosmosClient, isPostTxFailure } from "./cosmosclient";
import { makeSignBytes } from "./encoding";
import { LcdClient } from "./lcdapi";
import { isMsgSend, MsgSend } from "./msgs";
import { Secp256k1Pen } from "./pen";
import { RestClient } from "./restclient";
import { SigningCosmosClient } from "./signingcosmosclient";
import {
faucet,
@ -86,7 +86,7 @@ describe("CosmosClient.searchTx", () => {
const transferAmount = coins(1234567, "ucosm");
const result = await client.sendTokens(recipient, transferAmount);
await sleep(75); // wait until tx is indexed
const txDetails = await new RestClient(wasmd.endpoint).txById(result.transactionHash);
const txDetails = await new LcdClient(wasmd.endpoint).txById(result.transactionHash);
sendSuccessful = {
sender: faucet.address,
recipient: recipient,

View File

@ -43,7 +43,7 @@ describe("CosmosClient", () => {
pendingWithoutWasmd();
const client = new CosmosClient(wasmd.endpoint);
const openedClient = (client as unknown) as PrivateCosmWasmClient;
const getCodeSpy = spyOn(openedClient.restClient, "nodeInfo").and.callThrough();
const getCodeSpy = spyOn(openedClient.lcdClient, "nodeInfo").and.callThrough();
expect(await client.getChainId()).toEqual(wasmd.chainId); // from network
expect(await client.getChainId()).toEqual(wasmd.chainId); // from cache
@ -57,7 +57,7 @@ describe("CosmosClient", () => {
pendingWithoutWasmd();
const client = new CosmosClient(wasmd.endpoint);
const openedClient = (client as unknown) as PrivateCosmWasmClient;
const blockLatestSpy = spyOn(openedClient.restClient, "blocksLatest").and.callThrough();
const blockLatestSpy = spyOn(openedClient.lcdClient, "blocksLatest").and.callThrough();
const height1 = await client.getHeight();
expect(height1).toBeGreaterThan(0);
@ -74,8 +74,8 @@ describe("CosmosClient", () => {
const client = new CosmosClient(wasmd.endpoint);
const openedClient = (client as unknown) as PrivateCosmWasmClient;
const blockLatestSpy = spyOn(openedClient.restClient, "blocksLatest").and.callThrough();
const authAccountsSpy = spyOn(openedClient.restClient, "authAccounts").and.callThrough();
const blockLatestSpy = spyOn(openedClient.lcdClient, "blocksLatest").and.callThrough();
const authAccountsSpy = spyOn(openedClient.lcdClient.auth, "account").and.callThrough();
const height1 = await client.getHeight();
expect(height1).toBeGreaterThan(0);

View File

@ -3,9 +3,9 @@ import { fromBase64, fromHex, toHex } from "@cosmjs/encoding";
import { Uint53 } from "@cosmjs/math";
import { Coin } from "./coins";
import { AuthExtension, BroadcastMode, LcdClient, setupAuthExtension } from "./lcdapi";
import { Log, parseLogs } from "./logs";
import { decodeBech32Pubkey } from "./pubkey";
import { BroadcastMode, RestClient } from "./restclient";
import { CosmosSdkTx, PubKey, StdTx } from "./types";
export interface GetNonceResult {
@ -130,11 +130,11 @@ export interface Block {
/** Use for testing only */
export interface PrivateCosmWasmClient {
readonly restClient: RestClient;
readonly lcdClient: LcdClient & AuthExtension;
}
export class CosmosClient {
protected readonly restClient: RestClient;
protected readonly lcdClient: LcdClient & AuthExtension;
/** Any address the chain considers valid (valid bech32 with proper prefix) */
protected anyValidAddress: string | undefined;
@ -150,12 +150,15 @@ export class CosmosClient {
* @param broadcastMode Defines at which point of the transaction processing the postTx method (i.e. transaction broadcasting) returns
*/
public constructor(apiUrl: string, broadcastMode = BroadcastMode.Block) {
this.restClient = new RestClient(apiUrl, broadcastMode);
this.lcdClient = LcdClient.withExtensions(
{ apiUrl: apiUrl, broadcastMode: broadcastMode },
setupAuthExtension,
);
}
public async getChainId(): Promise<string> {
if (!this.chainId) {
const response = await this.restClient.nodeInfo();
const response = await this.lcdClient.nodeInfo();
const chainId = response.node_info.network;
if (!chainId) throw new Error("Chain ID must not be empty");
this.chainId = chainId;
@ -166,12 +169,12 @@ export class CosmosClient {
public async getHeight(): Promise<number> {
if (this.anyValidAddress) {
const { height } = await this.restClient.authAccounts(this.anyValidAddress);
const { height } = await this.lcdClient.auth.account(this.anyValidAddress);
return parseInt(height, 10);
} else {
// Note: this gets inefficient when blocks contain a lot of transactions since it
// requires downloading and deserializing all transactions in the block.
const latest = await this.restClient.blocksLatest();
const latest = await this.lcdClient.blocksLatest();
return parseInt(latest.block.header.height, 10);
}
}
@ -181,7 +184,7 @@ export class CosmosClient {
*/
public async getIdentifier(tx: CosmosSdkTx): Promise<string> {
// We consult the REST API because we don't have a local amino encoder
const response = await this.restClient.encodeTx(tx);
const response = await this.lcdClient.encodeTx(tx);
const hash = new Sha256(fromBase64(response.tx)).digest();
return toHex(hash).toUpperCase();
}
@ -207,7 +210,7 @@ export class CosmosClient {
}
public async getAccount(address: string): Promise<Account | undefined> {
const account = await this.restClient.authAccounts(address);
const account = await this.lcdClient.auth.account(address);
const value = account.result.value;
if (value.address === "") {
return undefined;
@ -230,7 +233,7 @@ export class CosmosClient {
*/
public async getBlock(height?: number): Promise<Block> {
const response =
height !== undefined ? await this.restClient.blocks(height) : await this.restClient.blocksLatest();
height !== undefined ? await this.lcdClient.blocks(height) : await this.lcdClient.blocksLatest();
return {
id: response.block_id.hash,
@ -287,7 +290,7 @@ export class CosmosClient {
}
public async postTx(tx: StdTx): Promise<PostTxResult> {
const result = await this.restClient.postTx(tx);
const result = await this.lcdClient.postTx(tx);
if (!result.txhash.match(/^([0-9A-F][0-9A-F])+$/)) {
throw new Error("Received ill-formatted txhash. Must be non-empty upper-case hex");
}
@ -310,7 +313,7 @@ export class CosmosClient {
private async txsQuery(query: string): Promise<readonly IndexedTx[]> {
// TODO: we need proper pagination support
const limit = 100;
const result = await this.restClient.txsQuery(`${query}&limit=${limit}`);
const result = await this.lcdClient.txsQuery(`${query}&limit=${limit}`);
const pages = parseInt(result.page_total, 10);
if (pages > 1) {
throw new Error(

View File

@ -22,15 +22,21 @@ export {
export { makeSignBytes } from "./encoding";
export {
AuthAccountsResponse,
AuthExtension,
BlockResponse,
BroadcastMode,
EncodeTxResponse,
PostTxsResponse,
LcdApiArray,
LcdClient,
NodeInfoResponse,
RestClient,
normalizeLcdApiArray,
PostTxsResponse,
SearchTxsResponse,
setupAuthExtension,
setupSupplyExtension,
SupplyExtension,
TxsResponse,
} from "./restclient";
} from "./lcdapi";
export { isMsgDelegate, isMsgSend, Msg, MsgDelegate, MsgSend } from "./msgs";
export { Pen, Secp256k1Pen, makeCosmoshubPath } from "./pen";
export { decodeBech32Pubkey, encodeBech32Pubkey, encodeSecp256k1Pubkey } from "./pubkey";

View File

@ -0,0 +1,68 @@
/* eslint-disable @typescript-eslint/camelcase */
import { encodeBech32Pubkey } from "../pubkey";
import {
faucet,
makeRandomAddress,
nonNegativeIntegerMatcher,
pendingWithoutWasmd,
unused,
wasmd,
} from "../testutils.spec";
import { AuthExtension, setupAuthExtension } from "./auth";
import { LcdClient } from "./lcdclient";
function makeAuthClient(apiUrl: string): LcdClient & AuthExtension {
return LcdClient.withExtensions({ apiUrl }, setupAuthExtension);
}
describe("AuthExtension", () => {
it("works for unused account without pubkey", async () => {
pendingWithoutWasmd();
const client = makeAuthClient(wasmd.endpoint);
const { height, result } = await client.auth.account(unused.address);
expect(height).toMatch(nonNegativeIntegerMatcher);
expect(result).toEqual({
type: "cosmos-sdk/Account",
value: {
address: unused.address,
public_key: "", // not known to the chain
coins: [
{
amount: "1000000000",
denom: "ucosm",
},
{
amount: "1000000000",
denom: "ustake",
},
],
account_number: unused.accountNumber,
sequence: 0,
},
});
});
// This fails in the first test run if you forget to run `./scripts/wasmd/init.sh`
it("has correct pubkey for faucet", async () => {
pendingWithoutWasmd();
const client = makeAuthClient(wasmd.endpoint);
const { result } = await client.auth.account(faucet.address);
expect(result.value).toEqual(
jasmine.objectContaining({
public_key: encodeBech32Pubkey(faucet.pubkey, "cosmospub"),
}),
);
});
// This property is used by CosmWasmClient.getAccount
it("returns empty address for non-existent account", async () => {
pendingWithoutWasmd();
const client = makeAuthClient(wasmd.endpoint);
const nonExistentAccount = makeRandomAddress();
const { result } = await client.auth.account(nonExistentAccount);
expect(result).toEqual({
type: "cosmos-sdk/Account",
value: jasmine.objectContaining({ address: "" }),
});
});
});

View File

@ -0,0 +1,41 @@
import { Coin } from "../coins";
import { LcdClient } from "./lcdclient";
export interface CosmosSdkAccount {
/** Bech32 account address */
readonly address: string;
readonly coins: readonly Coin[];
/** Bech32 encoded pubkey */
readonly public_key: string;
readonly account_number: number;
readonly sequence: number;
}
export interface AuthAccountsResponse {
readonly height: string;
readonly result: {
readonly type: "cosmos-sdk/Account";
readonly value: CosmosSdkAccount;
};
}
export interface AuthExtension {
readonly auth: {
readonly account: (address: string) => Promise<AuthAccountsResponse>;
};
}
export function setupAuthExtension(base: LcdClient): AuthExtension {
return {
auth: {
account: async (address: string) => {
const path = `/auth/accounts/${address}`;
const responseData = await base.get(path);
if (responseData.result.type !== "cosmos-sdk/Account") {
throw new Error("Unexpected response data format");
}
return responseData as AuthAccountsResponse;
},
},
};
}

View File

@ -0,0 +1,145 @@
import { CosmosSdkTx } from "../types";
/**
* The mode used to send transaction
*
* @see https://cosmos.network/rpc/#/Transactions/post_txs
*/
export enum BroadcastMode {
/** Return after tx commit */
Block = "block",
/** Return after CheckTx */
Sync = "sync",
/** Return right away */
Async = "async",
}
/** A response from the /txs/encode endpoint */
export interface EncodeTxResponse {
/** base64-encoded amino-binary encoded representation */
readonly tx: string;
}
interface NodeInfo {
readonly protocol_version: {
readonly p2p: string;
readonly block: string;
readonly app: string;
};
readonly id: string;
readonly listen_addr: string;
readonly network: string;
readonly version: string;
readonly channels: string;
readonly moniker: string;
readonly other: {
readonly tx_index: string;
readonly rpc_address: string;
};
}
interface ApplicationVersion {
readonly name: string;
readonly server_name: string;
readonly client_name: string;
readonly version: string;
readonly commit: string;
readonly build_tags: string;
readonly go: string;
}
export interface NodeInfoResponse {
readonly node_info: NodeInfo;
readonly application_version: ApplicationVersion;
}
interface BlockId {
readonly hash: string;
// TODO: here we also have this
// parts: {
// total: '1',
// hash: '7AF200C78FBF9236944E1AB270F4045CD60972B7C265E3A9DA42973397572931'
// }
}
interface BlockHeader {
readonly version: {
readonly block: string;
readonly app: string;
};
readonly height: string;
readonly chain_id: string;
/** An RFC 3339 time string like e.g. '2020-02-15T10:39:10.4696305Z' */
readonly time: string;
readonly last_commit_hash: string;
readonly last_block_id: BlockId;
/** Can be empty */
readonly data_hash: string;
readonly validators_hash: string;
readonly next_validators_hash: string;
readonly consensus_hash: string;
readonly app_hash: string;
/** Can be empty */
readonly last_results_hash: string;
/** Can be empty */
readonly evidence_hash: string;
readonly proposer_address: string;
}
interface Block {
readonly header: BlockHeader;
readonly data: {
/** Array of base64 encoded transactions */
readonly txs: readonly string[] | null;
};
}
export interface BlockResponse {
readonly block_id: BlockId;
readonly block: Block;
}
export interface TxsResponse {
readonly height: string;
readonly txhash: string;
/** 🤷‍♂️ */
readonly codespace?: string;
/** Falsy when transaction execution succeeded. Contains error code on error. */
readonly code?: number;
readonly raw_log: string;
readonly logs?: object;
readonly tx: CosmosSdkTx;
/** The gas limit as set by the user */
readonly gas_wanted?: string;
/** The gas used by the execution */
readonly gas_used?: string;
readonly timestamp: string;
}
export interface SearchTxsResponse {
readonly total_count: string;
readonly count: string;
readonly page_number: string;
readonly page_total: string;
readonly limit: string;
readonly txs: readonly TxsResponse[];
}
export interface PostTxsResponse {
readonly height: string;
readonly txhash: string;
readonly code?: number;
/**
* The result data of the execution (hex encoded).
*
* @see https://github.com/cosmos/cosmos-sdk/blob/v0.38.4/types/result.go#L101
*/
readonly data?: string;
readonly raw_log?: string;
/** The same as `raw_log` but deserialized? */
readonly logs?: object;
/** The gas limit as set by the user */
readonly gas_wanted?: string;
/** The gas used by the execution */
readonly gas_used?: string;
}

View File

@ -0,0 +1,21 @@
//
// Standard modules (see tracking issue https://github.com/CosmWasm/cosmjs/issues/276)
//
export { AuthExtension, AuthAccountsResponse, setupAuthExtension } from "./auth";
export { setupSupplyExtension, SupplyExtension, TotalSupplyAllResponse, TotalSupplyResponse } from "./supply";
//
// Base types
//
export {
BlockResponse,
BroadcastMode,
EncodeTxResponse,
PostTxsResponse,
NodeInfoResponse,
SearchTxsResponse,
TxsResponse,
} from "./base";
export { LcdApiArray, LcdClient, normalizeLcdApiArray } from "./lcdclient";

View File

@ -1,200 +1,199 @@
/* eslint-disable @typescript-eslint/camelcase */
import { assert, sleep } from "@cosmjs/utils";
import { ReadonlyDate } from "readonly-date";
import { rawSecp256k1PubkeyToAddress } from "./address";
import { isPostTxFailure } from "./cosmosclient";
import { makeSignBytes } from "./encoding";
import { parseLogs } from "./logs";
import { Msg, MsgSend } from "./msgs";
import { makeCosmoshubPath, Secp256k1Pen } from "./pen";
import { encodeBech32Pubkey } from "./pubkey";
import { RestClient, TxsResponse } from "./restclient";
import { SigningCosmosClient } from "./signingcosmosclient";
import cosmoshub from "./testdata/cosmoshub.json";
import { rawSecp256k1PubkeyToAddress } from "../address";
import { Coin } from "../coins";
import { isPostTxFailure } from "../cosmosclient";
import { makeSignBytes } from "../encoding";
import { parseLogs } from "../logs";
import { MsgSend } from "../msgs";
import { makeCosmoshubPath, Secp256k1Pen } from "../pen";
import { SigningCosmosClient } from "../signingcosmosclient";
import cosmoshub from "../testdata/cosmoshub.json";
import {
faucet,
makeRandomAddress,
makeSignedTx,
nonNegativeIntegerMatcher,
pendingWithoutWasmd,
semverMatcher,
tendermintAddressMatcher,
tendermintIdMatcher,
tendermintOptionalIdMatcher,
tendermintShortHashMatcher,
unused,
wasmd,
wasmdEnabled,
} from "./testutils.spec";
import { StdFee, StdSignature, StdTx } from "./types";
} from "../testutils.spec";
import { StdFee } from "../types";
import { setupAuthExtension } from "./auth";
import { TxsResponse } from "./base";
import { LcdApiArray, LcdClient, normalizeLcdApiArray } from "./lcdclient";
const emptyAddress = "cosmos1ltkhnmdcqemmd2tkhnx7qx66tq7e0wykw2j85k";
/** Deployed as part of scripts/wasmd/init.sh */
export const deployedErc20 = {
codeId: 1,
source: "https://crates.io/api/v1/crates/cw-erc20/0.5.1/download",
builder: "cosmwasm/rust-optimizer:0.8.0",
checksum: "3e97bf88bd960fee5e5959c77b972eb2927690bc10160792741b174f105ec0c5",
instances: [
"cosmos18vd8fpwxzck93qlwghaj6arh4p7c5n89uzcee5", // HASH
"cosmos1hqrdl6wstt8qzshwc6mrumpjk9338k0lr4dqxd", // ISA
"cosmos18r5szma8hm93pvx6lwpjwyxruw27e0k5uw835c", // JADE
],
};
function makeSignedTx(firstMsg: Msg, fee: StdFee, memo: string, firstSignature: StdSignature): StdTx {
return {
msg: [firstMsg],
fee: fee,
memo: memo,
signatures: [firstSignature],
};
}
describe("LcdClient", () => {
const defaultRecipientAddress = makeRandomAddress();
describe("RestClient", () => {
it("can be constructed", () => {
const client = new RestClient(wasmd.endpoint);
const client = new LcdClient(wasmd.endpoint);
expect(client).toBeTruthy();
});
// The /auth endpoints
describe("withModules", () => {
interface CodeInfo {
readonly id: number;
/** Bech32 account address */
readonly creator: string;
/** Hex-encoded sha256 hash of the code stored here */
readonly data_hash: string;
readonly source?: string;
readonly builder?: string;
}
describe("authAccounts", () => {
it("works for unused account without pubkey", async () => {
pendingWithoutWasmd();
const client = new RestClient(wasmd.endpoint);
const { height, result } = await client.authAccounts(unused.address);
expect(height).toMatch(nonNegativeIntegerMatcher);
expect(result).toEqual({
type: "cosmos-sdk/Account",
value: {
address: unused.address,
public_key: "", // not known to the chain
coins: [
{
amount: "1000000000",
denom: "ucosm",
},
{
amount: "1000000000",
denom: "ustake",
},
],
account_number: unused.accountNumber,
sequence: 0,
type WasmResponse<T> = WasmSuccess<T> | WasmError;
interface WasmSuccess<T> {
readonly height: string;
readonly result: T;
}
interface WasmError {
readonly error: string;
}
function isWasmError<T>(resp: WasmResponse<T>): resp is WasmError {
return (resp as WasmError).error !== undefined;
}
function unwrapWasmResponse<T>(response: WasmResponse<T>): T {
if (isWasmError(response)) {
throw new Error(response.error);
}
return response.result;
}
interface WasmExtension {
wasm: {
listCodeInfo: () => Promise<readonly CodeInfo[]>;
};
}
function setupWasmExtension(base: LcdClient): WasmExtension {
return {
wasm: {
listCodeInfo: async (): Promise<readonly CodeInfo[]> => {
const path = `/wasm/code`;
const responseData = (await base.get(path)) as WasmResponse<LcdApiArray<CodeInfo>>;
return normalizeLcdApiArray(unwrapWasmResponse(responseData));
},
},
});
};
}
it("works for no extension", async () => {
const client = LcdClient.withExtensions({ apiUrl: wasmd.endpoint });
expect(client).toBeTruthy();
});
// This fails in the first test run if you forget to run `./scripts/wasmd/init.sh`
it("has correct pubkey for faucet", async () => {
it("works for one extension", async () => {
pendingWithoutWasmd();
const client = new RestClient(wasmd.endpoint);
const { result } = await client.authAccounts(faucet.address);
expect(result.value).toEqual(
jasmine.objectContaining({
public_key: encodeBech32Pubkey(faucet.pubkey, "cosmospub"),
}),
const client = LcdClient.withExtensions({ apiUrl: wasmd.endpoint }, setupWasmExtension);
const codes = await client.wasm.listCodeInfo();
expect(codes.length).toBeGreaterThanOrEqual(3);
expect(codes[0].id).toEqual(deployedErc20.codeId);
expect(codes[0].data_hash).toEqual(deployedErc20.checksum.toUpperCase());
expect(codes[0].builder).toEqual(deployedErc20.builder);
expect(codes[0].source).toEqual(deployedErc20.source);
});
it("works for two extensions", async () => {
pendingWithoutWasmd();
interface TotalSupplyAllResponse {
readonly height: string;
readonly result: LcdApiArray<Coin>;
}
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
function setupSupplyExtension(base: LcdClient) {
return {
supply: {
totalAll: async (): Promise<TotalSupplyAllResponse> => {
const path = `/supply/total`;
return (await base.get(path)) as TotalSupplyAllResponse;
},
},
};
}
const client = LcdClient.withExtensions(
{ apiUrl: wasmd.endpoint },
setupWasmExtension,
setupSupplyExtension,
);
});
// This property is used by CosmWasmClient.getAccount
it("returns empty address for non-existent account", async () => {
pendingWithoutWasmd();
const client = new RestClient(wasmd.endpoint);
const nonExistentAccount = makeRandomAddress();
const { result } = await client.authAccounts(nonExistentAccount);
expect(result).toEqual({
type: "cosmos-sdk/Account",
value: jasmine.objectContaining({ address: "" }),
const codes = await client.wasm.listCodeInfo();
expect(codes.length).toBeGreaterThanOrEqual(3);
expect(codes[0].id).toEqual(deployedErc20.codeId);
expect(codes[0].data_hash).toEqual(deployedErc20.checksum.toUpperCase());
expect(codes[0].builder).toEqual(deployedErc20.builder);
expect(codes[0].source).toEqual(deployedErc20.source);
const supply = await client.supply.totalAll();
expect(supply).toEqual({
height: jasmine.stringMatching(/^[0-9]+$/),
result: [
{
amount: jasmine.stringMatching(/^[0-9]+$/),
denom: "ucosm",
},
{
amount: jasmine.stringMatching(/^[0-9]+$/),
denom: "ustake",
},
],
});
});
});
// The /blocks endpoints
describe("blocksLatest", () => {
it("works", async () => {
it("can merge two extensions into the same module", async () => {
pendingWithoutWasmd();
const client = new RestClient(wasmd.endpoint);
const response = await client.blocksLatest();
// id
expect(response.block_id.hash).toMatch(tendermintIdMatcher);
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
function setupSupplyExtensionBasic(base: LcdClient) {
return {
supply: {
totalAll: async () => {
const path = `/supply/total`;
return base.get(path);
},
},
};
}
// header
expect(response.block.header.version).toEqual({ block: "10", app: "0" });
expect(parseInt(response.block.header.height, 10)).toBeGreaterThanOrEqual(1);
expect(response.block.header.chain_id).toEqual(wasmd.chainId);
expect(new ReadonlyDate(response.block.header.time).getTime()).toBeLessThan(ReadonlyDate.now());
expect(new ReadonlyDate(response.block.header.time).getTime()).toBeGreaterThanOrEqual(
ReadonlyDate.now() - 5_000,
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
function setupSupplyExtensionPremium(base: LcdClient) {
return {
supply: {
total: async (denom: string) => {
return base.get(`/supply/total/${denom}`);
},
},
};
}
const client = LcdClient.withExtensions(
{ apiUrl: wasmd.endpoint },
setupSupplyExtensionBasic,
setupSupplyExtensionPremium,
);
expect(response.block.header.last_commit_hash).toMatch(tendermintIdMatcher);
expect(response.block.header.last_block_id.hash).toMatch(tendermintIdMatcher);
expect(response.block.header.data_hash).toMatch(tendermintOptionalIdMatcher);
expect(response.block.header.validators_hash).toMatch(tendermintIdMatcher);
expect(response.block.header.next_validators_hash).toMatch(tendermintIdMatcher);
expect(response.block.header.consensus_hash).toMatch(tendermintIdMatcher);
expect(response.block.header.app_hash).toMatch(tendermintIdMatcher);
expect(response.block.header.last_results_hash).toMatch(tendermintOptionalIdMatcher);
expect(response.block.header.evidence_hash).toMatch(tendermintOptionalIdMatcher);
expect(response.block.header.proposer_address).toMatch(tendermintAddressMatcher);
// data
expect(response.block.data.txs === null || Array.isArray(response.block.data.txs)).toEqual(true);
});
});
describe("blocks", () => {
it("works for block by height", async () => {
pendingWithoutWasmd();
const client = new RestClient(wasmd.endpoint);
const height = parseInt((await client.blocksLatest()).block.header.height, 10);
const response = await client.blocks(height - 1);
// id
expect(response.block_id.hash).toMatch(tendermintIdMatcher);
// header
expect(response.block.header.version).toEqual({ block: "10", app: "0" });
expect(response.block.header.height).toEqual(`${height - 1}`);
expect(response.block.header.chain_id).toEqual(wasmd.chainId);
expect(new ReadonlyDate(response.block.header.time).getTime()).toBeLessThan(ReadonlyDate.now());
expect(new ReadonlyDate(response.block.header.time).getTime()).toBeGreaterThanOrEqual(
ReadonlyDate.now() - 5_000,
);
expect(response.block.header.last_commit_hash).toMatch(tendermintIdMatcher);
expect(response.block.header.last_block_id.hash).toMatch(tendermintIdMatcher);
expect(response.block.header.data_hash).toMatch(tendermintOptionalIdMatcher);
expect(response.block.header.validators_hash).toMatch(tendermintIdMatcher);
expect(response.block.header.next_validators_hash).toMatch(tendermintIdMatcher);
expect(response.block.header.consensus_hash).toMatch(tendermintIdMatcher);
expect(response.block.header.app_hash).toMatch(tendermintIdMatcher);
expect(response.block.header.last_results_hash).toMatch(tendermintOptionalIdMatcher);
expect(response.block.header.evidence_hash).toMatch(tendermintOptionalIdMatcher);
expect(response.block.header.proposer_address).toMatch(tendermintAddressMatcher);
// data
expect(response.block.data.txs === null || Array.isArray(response.block.data.txs)).toEqual(true);
});
});
// The /node_info endpoint
describe("nodeInfo", () => {
it("works", async () => {
pendingWithoutWasmd();
const client = new RestClient(wasmd.endpoint);
const { node_info, application_version } = await client.nodeInfo();
expect(node_info).toEqual({
protocol_version: { p2p: "7", block: "10", app: "0" },
id: jasmine.stringMatching(tendermintShortHashMatcher),
listen_addr: "tcp://0.0.0.0:26656",
network: wasmd.chainId,
version: jasmine.stringMatching(/^0\.33\.[0-9]+$/),
channels: "4020212223303800",
moniker: wasmd.chainId,
other: { tx_index: "on", rpc_address: "tcp://0.0.0.0:26657" },
});
expect(application_version).toEqual({
name: "wasm",
server_name: "wasmd",
client_name: "wasmcli",
version: jasmine.stringMatching(semverMatcher),
commit: jasmine.stringMatching(tendermintShortHashMatcher),
build_tags: "netgo,ledger,muslc",
go: jasmine.stringMatching(/^go version go1\.[0-9]+\.[0-9]+ linux\/amd64$/),
});
expect(client.supply.totalAll).toEqual(jasmine.any(Function));
expect(client.supply.total).toEqual(jasmine.any(Function));
});
});
@ -292,7 +291,7 @@ describe("RestClient", () => {
it("works for successful transaction", async () => {
pendingWithoutWasmd();
assert(successful);
const client = new RestClient(wasmd.endpoint);
const client = new LcdClient(wasmd.endpoint);
const result = await client.txById(successful.hash);
expect(result.height).toBeGreaterThanOrEqual(1);
expect(result.txhash).toEqual(successful.hash);
@ -328,7 +327,7 @@ describe("RestClient", () => {
it("works for unsuccessful transaction", async () => {
pendingWithoutWasmd();
assert(unsuccessful);
const client = new RestClient(wasmd.endpoint);
const client = new LcdClient(wasmd.endpoint);
const result = await client.txById(unsuccessful.hash);
expect(result.height).toBeGreaterThanOrEqual(1);
expect(result.txhash).toEqual(unsuccessful.hash);
@ -367,7 +366,7 @@ describe("RestClient", () => {
const result = await client.sendTokens(recipient, transferAmount);
await sleep(75); // wait until tx is indexed
const txDetails = await new RestClient(wasmd.endpoint).txById(result.transactionHash);
const txDetails = await new LcdClient(wasmd.endpoint).txById(result.transactionHash);
posted = {
sender: faucet.address,
recipient: recipient,
@ -381,7 +380,7 @@ describe("RestClient", () => {
it("can query transactions by height", async () => {
pendingWithoutWasmd();
assert(posted);
const client = new RestClient(wasmd.endpoint);
const client = new LcdClient(wasmd.endpoint);
const result = await client.txsQuery(`tx.height=${posted.height}&limit=26`);
expect(result).toEqual({
count: jasmine.stringMatching(/^(1|2|3|4|5)$/), // 1-5 transactions as string
@ -396,7 +395,7 @@ describe("RestClient", () => {
it("can query transactions by ID", async () => {
pendingWithoutWasmd();
assert(posted);
const client = new RestClient(wasmd.endpoint);
const client = new LcdClient(wasmd.endpoint);
const result = await client.txsQuery(`tx.hash=${posted.hash}&limit=26`);
expect(result).toEqual({
count: "1",
@ -411,7 +410,7 @@ describe("RestClient", () => {
it("can query transactions by sender", async () => {
pendingWithoutWasmd();
assert(posted);
const client = new RestClient(wasmd.endpoint);
const client = new LcdClient(wasmd.endpoint);
const result = await client.txsQuery(`message.sender=${posted.sender}&limit=200`);
expect(parseInt(result.count, 10)).toBeGreaterThanOrEqual(1);
expect(parseInt(result.limit, 10)).toEqual(200);
@ -425,7 +424,7 @@ describe("RestClient", () => {
it("can query transactions by recipient", async () => {
pendingWithoutWasmd();
assert(posted);
const client = new RestClient(wasmd.endpoint);
const client = new LcdClient(wasmd.endpoint);
const result = await client.txsQuery(`transfer.recipient=${posted.recipient}&limit=200`);
expect(parseInt(result.count, 10)).toEqual(1);
expect(parseInt(result.limit, 10)).toEqual(200);
@ -440,7 +439,7 @@ describe("RestClient", () => {
pending("This combination is broken 🤷‍♂️. Handle client-side at higher level.");
pendingWithoutWasmd();
assert(posted);
const client = new RestClient(wasmd.endpoint);
const client = new LcdClient(wasmd.endpoint);
const hashQuery = `tx.hash=${posted.hash}`;
{
@ -467,7 +466,7 @@ describe("RestClient", () => {
it("can filter by recipient and tx.minheight", async () => {
pendingWithoutWasmd();
assert(posted);
const client = new RestClient(wasmd.endpoint);
const client = new LcdClient(wasmd.endpoint);
const recipientQuery = `transfer.recipient=${posted.recipient}`;
{
@ -494,7 +493,7 @@ describe("RestClient", () => {
it("can filter by recipient and tx.maxheight", async () => {
pendingWithoutWasmd();
assert(posted);
const client = new RestClient(wasmd.endpoint);
const client = new LcdClient(wasmd.endpoint);
const recipientQuery = `transfer.recipient=${posted.recipient}`;
{
@ -522,7 +521,7 @@ describe("RestClient", () => {
describe("encodeTx", () => {
it("works for cosmoshub example", async () => {
pendingWithoutWasmd();
const client = new RestClient(wasmd.endpoint);
const client = new LcdClient(wasmd.endpoint);
const response = await client.encodeTx(cosmoshub.tx);
expect(response).toEqual(
jasmine.objectContaining({
@ -542,7 +541,7 @@ describe("RestClient", () => {
type: "cosmos-sdk/MsgSend",
value: {
from_address: faucet.address,
to_address: emptyAddress,
to_address: defaultRecipientAddress,
amount: [
{
denom: "ucosm",
@ -562,8 +561,8 @@ describe("RestClient", () => {
gas: "890000",
};
const client = new RestClient(wasmd.endpoint);
const { account_number, sequence } = (await client.authAccounts(faucet.address)).result.value;
const client = LcdClient.withExtensions({ apiUrl: wasmd.endpoint }, setupAuthExtension);
const { account_number, sequence } = (await client.auth.account(faucet.address)).result.value;
const signBytes = makeSignBytes([theMsg], fee, wasmd.chainId, memo, account_number, sequence);
const signature = await pen.sign(signBytes);
@ -595,7 +594,7 @@ describe("RestClient", () => {
type: "cosmos-sdk/MsgSend",
value: {
from_address: address1,
to_address: emptyAddress,
to_address: defaultRecipientAddress,
amount: [
{
denom: "ucosm",
@ -615,10 +614,10 @@ describe("RestClient", () => {
gas: "890000",
};
const client = new RestClient(wasmd.endpoint);
const { account_number: an1, sequence: sequence1 } = (await client.authAccounts(address1)).result.value;
const { account_number: an2, sequence: sequence2 } = (await client.authAccounts(address2)).result.value;
const { account_number: an3, sequence: sequence3 } = (await client.authAccounts(address3)).result.value;
const client = LcdClient.withExtensions({ apiUrl: wasmd.endpoint }, setupAuthExtension);
const { account_number: an1, sequence: sequence1 } = (await client.auth.account(address1)).result.value;
const { account_number: an2, sequence: sequence2 } = (await client.auth.account(address2)).result.value;
const { account_number: an3, sequence: sequence3 } = (await client.auth.account(address3)).result.value;
const signBytes1 = makeSignBytes([theMsg], fee, wasmd.chainId, memo, an1, sequence1);
const signBytes2 = makeSignBytes([theMsg], fee, wasmd.chainId, memo, an2, sequence2);
@ -647,7 +646,7 @@ describe("RestClient", () => {
type: "cosmos-sdk/MsgSend",
value: {
from_address: address1,
to_address: emptyAddress,
to_address: defaultRecipientAddress,
amount: [
{
denom: "ucosm",
@ -660,7 +659,7 @@ describe("RestClient", () => {
type: "cosmos-sdk/MsgSend",
value: {
from_address: address1,
to_address: emptyAddress,
to_address: defaultRecipientAddress,
amount: [
{
denom: "ucosm",
@ -680,8 +679,8 @@ describe("RestClient", () => {
gas: "890000",
};
const client = new RestClient(wasmd.endpoint);
const { account_number, sequence } = (await client.authAccounts(address1)).result.value;
const client = LcdClient.withExtensions({ apiUrl: wasmd.endpoint }, setupAuthExtension);
const { account_number, sequence } = (await client.auth.account(address1)).result.value;
const signBytes = makeSignBytes([msg1, msg2], fee, wasmd.chainId, memo, account_number, sequence);
const signature1 = await account1.sign(signBytes);
@ -707,7 +706,7 @@ describe("RestClient", () => {
type: "cosmos-sdk/MsgSend",
value: {
from_address: address1,
to_address: emptyAddress,
to_address: defaultRecipientAddress,
amount: [
{
denom: "ucosm",
@ -720,7 +719,7 @@ describe("RestClient", () => {
type: "cosmos-sdk/MsgSend",
value: {
from_address: address2,
to_address: emptyAddress,
to_address: defaultRecipientAddress,
amount: [
{
denom: "ucosm",
@ -740,9 +739,9 @@ describe("RestClient", () => {
gas: "890000",
};
const client = new RestClient(wasmd.endpoint);
const { account_number: an1, sequence: sequence1 } = (await client.authAccounts(address1)).result.value;
const { account_number: an2, sequence: sequence2 } = (await client.authAccounts(address2)).result.value;
const client = LcdClient.withExtensions({ apiUrl: wasmd.endpoint }, setupAuthExtension);
const { account_number: an1, sequence: sequence1 } = (await client.auth.account(address1)).result.value;
const { account_number: an2, sequence: sequence2 } = (await client.auth.account(address2)).result.value;
const signBytes1 = makeSignBytes([msg2, msg1], fee, wasmd.chainId, memo, an1, sequence1);
const signBytes2 = makeSignBytes([msg2, msg1], fee, wasmd.chainId, memo, an2, sequence2);
@ -775,7 +774,7 @@ describe("RestClient", () => {
type: "cosmos-sdk/MsgSend",
value: {
from_address: address1,
to_address: emptyAddress,
to_address: defaultRecipientAddress,
amount: [
{
denom: "ucosm",
@ -788,7 +787,7 @@ describe("RestClient", () => {
type: "cosmos-sdk/MsgSend",
value: {
from_address: address2,
to_address: emptyAddress,
to_address: defaultRecipientAddress,
amount: [
{
denom: "ucosm",
@ -808,9 +807,9 @@ describe("RestClient", () => {
gas: "890000",
};
const client = new RestClient(wasmd.endpoint);
const { account_number: an1, sequence: sequence1 } = (await client.authAccounts(address1)).result.value;
const { account_number: an2, sequence: sequence2 } = (await client.authAccounts(address2)).result.value;
const client = LcdClient.withExtensions({ apiUrl: wasmd.endpoint }, setupAuthExtension);
const { account_number: an1, sequence: sequence1 } = (await client.auth.account(address1)).result.value;
const { account_number: an2, sequence: sequence2 } = (await client.auth.account(address2)).result.value;
const signBytes1 = makeSignBytes([msg1, msg2], fee, wasmd.chainId, memo, an1, sequence1);
const signBytes2 = makeSignBytes([msg1, msg2], fee, wasmd.chainId, memo, an2, sequence2);
@ -838,7 +837,7 @@ describe("RestClient", () => {
type: "cosmos-sdk/MsgSend",
value: {
from_address: address1,
to_address: emptyAddress,
to_address: defaultRecipientAddress,
amount: [
{
denom: "ucosm",
@ -851,7 +850,7 @@ describe("RestClient", () => {
type: "cosmos-sdk/MsgSend",
value: {
from_address: address2,
to_address: emptyAddress,
to_address: defaultRecipientAddress,
amount: [
{
denom: "ucosm",
@ -871,9 +870,9 @@ describe("RestClient", () => {
gas: "890000",
};
const client = new RestClient(wasmd.endpoint);
const { account_number: an1, sequence: sequence1 } = (await client.authAccounts(address1)).result.value;
const { account_number: an2, sequence: sequence2 } = (await client.authAccounts(address2)).result.value;
const client = LcdClient.withExtensions({ apiUrl: wasmd.endpoint }, setupAuthExtension);
const { account_number: an1, sequence: sequence1 } = (await client.auth.account(address1)).result.value;
const { account_number: an2, sequence: sequence2 } = (await client.auth.account(address2)).result.value;
const signBytes1 = makeSignBytes([msg2, msg1], fee, wasmd.chainId, memo, an1, sequence1);
const signBytes2 = makeSignBytes([msg2, msg1], fee, wasmd.chainId, memo, an2, sequence2);

View File

@ -0,0 +1,312 @@
/* eslint-disable no-dupe-class-members */
import { assert, isNonNullObject } from "@cosmjs/utils";
import axios, { AxiosError, AxiosInstance } from "axios";
import { CosmosSdkTx, StdTx } from "../types";
import {
BlockResponse,
BroadcastMode,
EncodeTxResponse,
NodeInfoResponse,
PostTxsResponse,
SearchTxsResponse,
TxsResponse,
} from "./base";
/** Unfortunately, Cosmos SDK encodes empty arrays as null */
export type LcdApiArray<T> = readonly T[] | null;
export function normalizeLcdApiArray<T>(backend: LcdApiArray<T>): readonly T[] {
return backend || [];
}
type LcdExtensionSetup<P> = (base: LcdClient) => P;
export interface LcdClientBaseOptions {
readonly apiUrl: string;
readonly broadcastMode?: BroadcastMode;
}
// We want to get message data from 500 errors
// https://stackoverflow.com/questions/56577124/how-to-handle-500-error-message-with-axios
// this should be chained to catch one error and throw a more informative one
function parseAxiosError(err: AxiosError): never {
// use the error message sent from server, not default 500 msg
if (err.response?.data) {
let errorText: string;
const data = err.response.data;
// expect { error: string }, but otherwise dump
if (data.error && typeof data.error === "string") {
errorText = data.error;
} else if (typeof data === "string") {
errorText = data;
} else {
errorText = JSON.stringify(data);
}
throw new Error(`${errorText} (HTTP ${err.response.status})`);
} else {
throw err;
}
}
/**
* A client to the LCD's (light client daemon) API.
* This light client connects to Tendermint (i.e. the chain), encodes/decodes Amino data for us and provides a convenient JSON interface.
*
* This _JSON over HTTP_ API is sometimes referred to as "REST" or "RPC", which are both misleading terms
* for the same thing.
*
* Please note that the client to the LCD can not verify light client proofs. When using this,
* you need to trust the API provider as well as the network connection between client and API.
*
* @see https://cosmos.network/rpc
*/
export class LcdClient {
/** Constructs an LCD client with 0 extensions */
public static withExtensions(options: LcdClientBaseOptions): LcdClient;
/** Constructs an LCD client with 1 extension */
public static withExtensions<A extends object>(
options: LcdClientBaseOptions,
setupExtensionA: LcdExtensionSetup<A>,
): LcdClient & A;
/** Constructs an LCD client with 2 extensions */
public static withExtensions<A extends object, B extends object>(
options: LcdClientBaseOptions,
setupExtensionA: LcdExtensionSetup<A>,
setupExtensionB: LcdExtensionSetup<B>,
): LcdClient & A & B;
/** Constructs an LCD client with 3 extensions */
public static withExtensions<A extends object, B extends object, C extends object>(
options: LcdClientBaseOptions,
setupExtensionA: LcdExtensionSetup<A>,
setupExtensionB: LcdExtensionSetup<B>,
setupExtensionC: LcdExtensionSetup<C>,
): LcdClient & A & B & C;
/** Constructs an LCD client with 4 extensions */
public static withExtensions<A extends object, B extends object, C extends object, D extends object>(
options: LcdClientBaseOptions,
setupExtensionA: LcdExtensionSetup<A>,
setupExtensionB: LcdExtensionSetup<B>,
setupExtensionC: LcdExtensionSetup<C>,
setupExtensionD: LcdExtensionSetup<D>,
): LcdClient & A & B & C & D;
/** Constructs an LCD client with 5 extensions */
public static withExtensions<
A extends object,
B extends object,
C extends object,
D extends object,
E extends object
>(
options: LcdClientBaseOptions,
setupExtensionA: LcdExtensionSetup<A>,
setupExtensionB: LcdExtensionSetup<B>,
setupExtensionC: LcdExtensionSetup<C>,
setupExtensionD: LcdExtensionSetup<D>,
setupExtensionE: LcdExtensionSetup<E>,
): LcdClient & A & B & C & D & E;
/** Constructs an LCD client with 6 extensions */
public static withExtensions<
A extends object,
B extends object,
C extends object,
D extends object,
E extends object,
F extends object
>(
options: LcdClientBaseOptions,
setupExtensionA: LcdExtensionSetup<A>,
setupExtensionB: LcdExtensionSetup<B>,
setupExtensionC: LcdExtensionSetup<C>,
setupExtensionD: LcdExtensionSetup<D>,
setupExtensionE: LcdExtensionSetup<E>,
setupExtensionF: LcdExtensionSetup<F>,
): LcdClient & A & B & C & D & E & F;
/** Constructs an LCD client with 7 extensions */
public static withExtensions<
A extends object,
B extends object,
C extends object,
D extends object,
E extends object,
F extends object,
G extends object
>(
options: LcdClientBaseOptions,
setupExtensionA: LcdExtensionSetup<A>,
setupExtensionB: LcdExtensionSetup<B>,
setupExtensionC: LcdExtensionSetup<C>,
setupExtensionD: LcdExtensionSetup<D>,
setupExtensionE: LcdExtensionSetup<E>,
setupExtensionF: LcdExtensionSetup<F>,
setupExtensionG: LcdExtensionSetup<G>,
): LcdClient & A & B & C & D & E & F & G;
/** Constructs an LCD client with 8 extensions */
public static withExtensions<
A extends object,
B extends object,
C extends object,
D extends object,
E extends object,
F extends object,
G extends object,
H extends object
>(
options: LcdClientBaseOptions,
setupExtensionA: LcdExtensionSetup<A>,
setupExtensionB: LcdExtensionSetup<B>,
setupExtensionC: LcdExtensionSetup<C>,
setupExtensionD: LcdExtensionSetup<D>,
setupExtensionE: LcdExtensionSetup<E>,
setupExtensionF: LcdExtensionSetup<F>,
setupExtensionG: LcdExtensionSetup<G>,
setupExtensionH: LcdExtensionSetup<H>,
): LcdClient & A & B & C & D & E & F & G & H;
public static withExtensions(
options: LcdClientBaseOptions,
...extensionSetups: Array<LcdExtensionSetup<object>>
): any {
const client = new LcdClient(options.apiUrl, options.broadcastMode);
const extensions = extensionSetups.map((setupExtension) => setupExtension(client));
for (const extension of extensions) {
assert(isNonNullObject(extension), `Extension must be a non-null object`);
for (const [moduleKey, moduleValue] of Object.entries(extension)) {
assert(
isNonNullObject(moduleValue),
`Module must be a non-null object. Found type ${typeof moduleValue} for module "${moduleKey}".`,
);
const current = (client as any)[moduleKey] || {};
(client as any)[moduleKey] = {
...current,
...moduleValue,
};
}
}
return client;
}
private readonly client: AxiosInstance;
private readonly broadcastMode: BroadcastMode;
/**
* Creates a new client to interact with a Cosmos SDK light client daemon.
* This class tries to be a direct mapping onto the API. Some basic decoding and normalizatin is done
* but things like caching are done at a higher level.
*
* When building apps, you should not need to use this class directly. If you do, this indicates a missing feature
* in higher level components. Feel free to raise an issue in this case.
*
* @param apiUrl The URL of a Cosmos SDK light client daemon API (sometimes called REST server or REST API)
* @param broadcastMode Defines at which point of the transaction processing the postTx method (i.e. transaction broadcasting) returns
*/
public constructor(apiUrl: string, broadcastMode = BroadcastMode.Block) {
const headers = {
post: { "Content-Type": "application/json" },
};
this.client = axios.create({
baseURL: apiUrl,
headers: headers,
});
this.broadcastMode = broadcastMode;
}
public async get(path: string): Promise<any> {
const { data } = await this.client.get(path).catch(parseAxiosError);
if (data === null) {
throw new Error("Received null response from server");
}
return data;
}
public async post(path: string, params: any): Promise<any> {
if (!isNonNullObject(params)) throw new Error("Got unexpected type of params. Expected object.");
const { data } = await this.client.post(path, params).catch(parseAxiosError);
if (data === null) {
throw new Error("Received null response from server");
}
return data;
}
// The /blocks endpoints
public async blocksLatest(): Promise<BlockResponse> {
const responseData = await this.get("/blocks/latest");
if (!responseData.block) {
throw new Error("Unexpected response data format");
}
return responseData as BlockResponse;
}
public async blocks(height: number): Promise<BlockResponse> {
const responseData = await this.get(`/blocks/${height}`);
if (!responseData.block) {
throw new Error("Unexpected response data format");
}
return responseData as BlockResponse;
}
// The /node_info endpoint
public async nodeInfo(): Promise<NodeInfoResponse> {
const responseData = await this.get("/node_info");
if (!responseData.node_info) {
throw new Error("Unexpected response data format");
}
return responseData as NodeInfoResponse;
}
// The /txs endpoints
public async txById(id: string): Promise<TxsResponse> {
const responseData = await this.get(`/txs/${id}`);
if (!responseData.tx) {
throw new Error("Unexpected response data format");
}
return responseData as TxsResponse;
}
public async txsQuery(query: string): Promise<SearchTxsResponse> {
const responseData = await this.get(`/txs?${query}`);
if (!responseData.txs) {
throw new Error("Unexpected response data format");
}
return responseData as SearchTxsResponse;
}
/** returns the amino-encoding of the transaction performed by the server */
public async encodeTx(tx: CosmosSdkTx): Promise<EncodeTxResponse> {
const responseData = await this.post("/txs/encode", tx);
if (!responseData.tx) {
throw new Error("Unexpected response data format");
}
return responseData as EncodeTxResponse;
}
/**
* Broadcasts a signed transaction to the transaction pool.
* Depending on the client's broadcast mode, this might or might
* wait for checkTx or deliverTx to be executed before returning.
*
* @param tx a signed transaction as StdTx (i.e. not wrapped in type/value container)
*/
public async postTx(tx: StdTx): Promise<PostTxsResponse> {
const params = {
tx: tx,
mode: this.broadcastMode,
};
const responseData = await this.post("/txs", params);
if (!responseData.txhash) {
throw new Error("Unexpected response data format");
}
return responseData as PostTxsResponse;
}
}

View File

@ -0,0 +1,40 @@
import { pendingWithoutWasmd, wasmd } from "../testutils.spec";
import { LcdClient } from "./lcdclient";
import { setupSupplyExtension } from "./supply";
describe("SupplyExtension", () => {
describe("totalAll", () => {
it("works", async () => {
pendingWithoutWasmd();
const client = LcdClient.withExtensions({ apiUrl: wasmd.endpoint }, setupSupplyExtension);
const supply = await client.supply.totalAll();
expect(supply).toEqual({
height: jasmine.stringMatching(/^[0-9]+$/),
result: [
{
amount: jasmine.stringMatching(/^[0-9]+$/),
denom: "ucosm",
},
{
amount: jasmine.stringMatching(/^[0-9]+$/),
denom: "ustake",
},
],
});
});
});
describe("total", () => {
it("works", async () => {
pendingWithoutWasmd();
const client = LcdClient.withExtensions({ apiUrl: wasmd.endpoint }, setupSupplyExtension);
const supply = await client.supply.total("ucosm");
expect(supply).toEqual({
height: jasmine.stringMatching(/^[0-9]+$/),
result: jasmine.stringMatching(/^[0-9]+$/),
});
});
});
});

View File

@ -0,0 +1,33 @@
import { Coin } from "../coins";
import { LcdApiArray, LcdClient } from "./lcdclient";
export interface TotalSupplyAllResponse {
readonly height: string;
readonly result: LcdApiArray<Coin>;
}
export interface TotalSupplyResponse {
readonly height: string;
/** The amount */
readonly result: string;
}
export interface SupplyExtension {
readonly supply: {
readonly totalAll: () => Promise<TotalSupplyAllResponse>;
readonly total: (denom: string) => Promise<TotalSupplyResponse>;
};
}
export function setupSupplyExtension(base: LcdClient): SupplyExtension {
return {
supply: {
totalAll: async () => {
return base.get(`/supply/total`);
},
total: async (denom: string) => {
return base.get(`/supply/total/${denom}`);
},
},
};
}

View File

@ -1,318 +0,0 @@
import { isNonNullObject } from "@cosmjs/utils";
import axios, { AxiosError, AxiosInstance } from "axios";
import { Coin } from "./coins";
import { CosmosSdkTx, StdTx } from "./types";
export interface CosmosSdkAccount {
/** Bech32 account address */
readonly address: string;
readonly coins: readonly Coin[];
/** Bech32 encoded pubkey */
readonly public_key: string;
readonly account_number: number;
readonly sequence: number;
}
interface NodeInfo {
readonly protocol_version: {
readonly p2p: string;
readonly block: string;
readonly app: string;
};
readonly id: string;
readonly listen_addr: string;
readonly network: string;
readonly version: string;
readonly channels: string;
readonly moniker: string;
readonly other: {
readonly tx_index: string;
readonly rpc_address: string;
};
}
interface ApplicationVersion {
readonly name: string;
readonly server_name: string;
readonly client_name: string;
readonly version: string;
readonly commit: string;
readonly build_tags: string;
readonly go: string;
}
export interface NodeInfoResponse {
readonly node_info: NodeInfo;
readonly application_version: ApplicationVersion;
}
interface BlockId {
readonly hash: string;
// TODO: here we also have this
// parts: {
// total: '1',
// hash: '7AF200C78FBF9236944E1AB270F4045CD60972B7C265E3A9DA42973397572931'
// }
}
interface BlockHeader {
readonly version: {
readonly block: string;
readonly app: string;
};
readonly height: string;
readonly chain_id: string;
/** An RFC 3339 time string like e.g. '2020-02-15T10:39:10.4696305Z' */
readonly time: string;
readonly last_commit_hash: string;
readonly last_block_id: BlockId;
/** Can be empty */
readonly data_hash: string;
readonly validators_hash: string;
readonly next_validators_hash: string;
readonly consensus_hash: string;
readonly app_hash: string;
/** Can be empty */
readonly last_results_hash: string;
/** Can be empty */
readonly evidence_hash: string;
readonly proposer_address: string;
}
interface Block {
readonly header: BlockHeader;
readonly data: {
/** Array of base64 encoded transactions */
readonly txs: readonly string[] | null;
};
}
export interface BlockResponse {
readonly block_id: BlockId;
readonly block: Block;
}
export interface AuthAccountsResponse {
readonly height: string;
readonly result: {
readonly type: "cosmos-sdk/Account";
readonly value: CosmosSdkAccount;
};
}
export interface TxsResponse {
readonly height: string;
readonly txhash: string;
/** 🤷‍♂️ */
readonly codespace?: string;
/** Falsy when transaction execution succeeded. Contains error code on error. */
readonly code?: number;
readonly raw_log: string;
readonly logs?: object;
readonly tx: CosmosSdkTx;
/** The gas limit as set by the user */
readonly gas_wanted?: string;
/** The gas used by the execution */
readonly gas_used?: string;
readonly timestamp: string;
}
export interface SearchTxsResponse {
readonly total_count: string;
readonly count: string;
readonly page_number: string;
readonly page_total: string;
readonly limit: string;
readonly txs: readonly TxsResponse[];
}
export interface PostTxsResponse {
readonly height: string;
readonly txhash: string;
readonly code?: number;
/**
* The result data of the execution (hex encoded).
*
* @see https://github.com/cosmos/cosmos-sdk/blob/v0.38.4/types/result.go#L101
*/
readonly data?: string;
readonly raw_log?: string;
/** The same as `raw_log` but deserialized? */
readonly logs?: object;
/** The gas limit as set by the user */
readonly gas_wanted?: string;
/** The gas used by the execution */
readonly gas_used?: string;
}
/** A reponse from the /txs/encode endpoint */
export interface EncodeTxResponse {
/** base64-encoded amino-binary encoded representation */
readonly tx: string;
}
/**
* The mode used to send transaction
*
* @see https://cosmos.network/rpc/#/Transactions/post_txs
*/
export enum BroadcastMode {
/** Return after tx commit */
Block = "block",
/** Return afer CheckTx */
Sync = "sync",
/** Return right away */
Async = "async",
}
// We want to get message data from 500 errors
// https://stackoverflow.com/questions/56577124/how-to-handle-500-error-message-with-axios
// this should be chained to catch one error and throw a more informative one
function parseAxiosError(err: AxiosError): never {
// use the error message sent from server, not default 500 msg
if (err.response?.data) {
let errorText: string;
const data = err.response.data;
// expect { error: string }, but otherwise dump
if (data.error && typeof data.error === "string") {
errorText = data.error;
} else if (typeof data === "string") {
errorText = data;
} else {
errorText = JSON.stringify(data);
}
throw new Error(`${errorText} (HTTP ${err.response.status})`);
} else {
throw err;
}
}
export class RestClient {
private readonly client: AxiosInstance;
private readonly broadcastMode: BroadcastMode;
/**
* Creates a new client to interact with a Cosmos SDK light client daemon.
* This class tries to be a direct mapping onto the API. Some basic decoding and normalizatin is done
* but things like caching are done at a higher level.
*
* When building apps, you should not need to use this class directly. If you do, this indicates a missing feature
* in higher level components. Feel free to raise an issue in this case.
*
* @param apiUrl The URL of a Cosmos SDK light client daemon API (sometimes called REST server or REST API)
* @param broadcastMode Defines at which point of the transaction processing the postTx method (i.e. transaction broadcasting) returns
*/
public constructor(apiUrl: string, broadcastMode = BroadcastMode.Block) {
const headers = {
post: { "Content-Type": "application/json" },
};
this.client = axios.create({
baseURL: apiUrl,
headers: headers,
});
this.broadcastMode = broadcastMode;
}
public async get(path: string): Promise<any> {
const { data } = await this.client.get(path).catch(parseAxiosError);
if (data === null) {
throw new Error("Received null response from server");
}
return data;
}
public async post(path: string, params: any): Promise<any> {
if (!isNonNullObject(params)) throw new Error("Got unexpected type of params. Expected object.");
const { data } = await this.client.post(path, params).catch(parseAxiosError);
if (data === null) {
throw new Error("Received null response from server");
}
return data;
}
// The /auth endpoints
public async authAccounts(address: string): Promise<AuthAccountsResponse> {
const path = `/auth/accounts/${address}`;
const responseData = await this.get(path);
if (responseData.result.type !== "cosmos-sdk/Account") {
throw new Error("Unexpected response data format");
}
return responseData as AuthAccountsResponse;
}
// The /blocks endpoints
public async blocksLatest(): Promise<BlockResponse> {
const responseData = await this.get("/blocks/latest");
if (!responseData.block) {
throw new Error("Unexpected response data format");
}
return responseData as BlockResponse;
}
public async blocks(height: number): Promise<BlockResponse> {
const responseData = await this.get(`/blocks/${height}`);
if (!responseData.block) {
throw new Error("Unexpected response data format");
}
return responseData as BlockResponse;
}
// The /node_info endpoint
public async nodeInfo(): Promise<NodeInfoResponse> {
const responseData = await this.get("/node_info");
if (!responseData.node_info) {
throw new Error("Unexpected response data format");
}
return responseData as NodeInfoResponse;
}
// The /txs endpoints
public async txById(id: string): Promise<TxsResponse> {
const responseData = await this.get(`/txs/${id}`);
if (!responseData.tx) {
throw new Error("Unexpected response data format");
}
return responseData as TxsResponse;
}
public async txsQuery(query: string): Promise<SearchTxsResponse> {
const responseData = await this.get(`/txs?${query}`);
if (!responseData.txs) {
throw new Error("Unexpected response data format");
}
return responseData as SearchTxsResponse;
}
/** returns the amino-encoding of the transaction performed by the server */
public async encodeTx(tx: CosmosSdkTx): Promise<EncodeTxResponse> {
const responseData = await this.post("/txs/encode", tx);
if (!responseData.tx) {
throw new Error("Unexpected response data format");
}
return responseData as EncodeTxResponse;
}
/**
* Broadcasts a signed transaction to into the transaction pool.
* Depending on the RestClient's broadcast mode, this might or might
* wait for checkTx or deliverTx to be executed before returning.
*
* @param tx a signed transaction as StdTx (i.e. not wrapped in type/value container)
*/
public async postTx(tx: StdTx): Promise<PostTxsResponse> {
const params = {
tx: tx,
mode: this.broadcastMode,
};
const responseData = await this.post("/txs", params);
if (!responseData.txhash) {
throw new Error("Unexpected response data format");
}
return responseData as PostTxsResponse;
}
}

View File

@ -34,8 +34,8 @@ describe("SigningCosmosClient", () => {
const client = new SigningCosmosClient(httpUrl, faucet.address, (signBytes) => pen.sign(signBytes));
const openedClient = (client as unknown) as PrivateCosmWasmClient;
const blockLatestSpy = spyOn(openedClient.restClient, "blocksLatest").and.callThrough();
const authAccountsSpy = spyOn(openedClient.restClient, "authAccounts").and.callThrough();
const blockLatestSpy = spyOn(openedClient.lcdClient, "blocksLatest").and.callThrough();
const authAccountsSpy = spyOn(openedClient.lcdClient.auth, "account").and.callThrough();
const height = await client.getHeight();
expect(height).toBeGreaterThan(0);

View File

@ -1,8 +1,8 @@
import { Coin, coins } from "./coins";
import { Account, CosmosClient, GetNonceResult, PostTxResult } from "./cosmosclient";
import { makeSignBytes } from "./encoding";
import { BroadcastMode } from "./lcdapi";
import { MsgSend } from "./msgs";
import { BroadcastMode } from "./restclient";
import { StdFee, StdSignature, StdTx } from "./types";
export interface SigningCallback {

View File

@ -1,6 +1,9 @@
import { Random } from "@cosmjs/crypto";
import { Bech32 } from "@cosmjs/encoding";
import { Msg } from "./msgs";
import { StdFee, StdSignature, StdTx } from "./types";
export function makeRandomAddress(): string {
return Bech32.encode("cosmos", Random.getBytes(20));
}
@ -56,3 +59,12 @@ export function fromOneElementArray<T>(elements: ArrayLike<T>): T {
if (elements.length !== 1) throw new Error(`Expected exactly one element but got ${elements.length}`);
return elements[0];
}
export function makeSignedTx(firstMsg: Msg, fee: StdFee, memo: string, firstSignature: StdSignature): StdTx {
return {
msg: [firstMsg],
fee: fee,
memo: memo,
signatures: [firstSignature],
};
}

View File

@ -1,6 +1,6 @@
import { Coin } from "./coins";
import { AuthExtension, BroadcastMode, LcdClient } from "./lcdapi";
import { Log } from "./logs";
import { BroadcastMode, RestClient } from "./restclient";
import { CosmosSdkTx, PubKey, StdTx } from "./types";
export interface GetNonceResult {
readonly accountNumber: number;
@ -94,10 +94,10 @@ export interface Block {
}
/** Use for testing only */
export interface PrivateCosmWasmClient {
readonly restClient: RestClient;
readonly lcdClient: LcdClient & AuthExtension;
}
export declare class CosmosClient {
protected readonly restClient: RestClient;
protected readonly lcdClient: LcdClient & AuthExtension;
/** Any address the chain considers valid (valid bech32 with proper prefix) */
protected anyValidAddress: string | undefined;
private chainId;

View File

@ -20,15 +20,21 @@ export {
export { makeSignBytes } from "./encoding";
export {
AuthAccountsResponse,
AuthExtension,
BlockResponse,
BroadcastMode,
EncodeTxResponse,
PostTxsResponse,
LcdApiArray,
LcdClient,
NodeInfoResponse,
RestClient,
normalizeLcdApiArray,
PostTxsResponse,
SearchTxsResponse,
setupAuthExtension,
setupSupplyExtension,
SupplyExtension,
TxsResponse,
} from "./restclient";
} from "./lcdapi";
export { isMsgDelegate, isMsgSend, Msg, MsgDelegate, MsgSend } from "./msgs";
export { Pen, Secp256k1Pen, makeCosmoshubPath } from "./pen";
export { decodeBech32Pubkey, encodeBech32Pubkey, encodeSecp256k1Pubkey } from "./pubkey";

24
packages/sdk38/types/lcdapi/auth.d.ts vendored Normal file
View File

@ -0,0 +1,24 @@
import { Coin } from "../coins";
import { LcdClient } from "./lcdclient";
export interface CosmosSdkAccount {
/** Bech32 account address */
readonly address: string;
readonly coins: readonly Coin[];
/** Bech32 encoded pubkey */
readonly public_key: string;
readonly account_number: number;
readonly sequence: number;
}
export interface AuthAccountsResponse {
readonly height: string;
readonly result: {
readonly type: "cosmos-sdk/Account";
readonly value: CosmosSdkAccount;
};
}
export interface AuthExtension {
readonly auth: {
readonly account: (address: string) => Promise<AuthAccountsResponse>;
};
}
export declare function setupAuthExtension(base: LcdClient): AuthExtension;

View File

@ -1,13 +1,21 @@
import { Coin } from "./coins";
import { CosmosSdkTx, StdTx } from "./types";
export interface CosmosSdkAccount {
/** Bech32 account address */
readonly address: string;
readonly coins: readonly Coin[];
/** Bech32 encoded pubkey */
readonly public_key: string;
readonly account_number: number;
readonly sequence: number;
import { CosmosSdkTx } from "../types";
/**
* The mode used to send transaction
*
* @see https://cosmos.network/rpc/#/Transactions/post_txs
*/
export declare enum BroadcastMode {
/** Return after tx commit */
Block = "block",
/** Return after CheckTx */
Sync = "sync",
/** Return right away */
Async = "async",
}
/** A response from the /txs/encode endpoint */
export interface EncodeTxResponse {
/** base64-encoded amino-binary encoded representation */
readonly tx: string;
}
interface NodeInfo {
readonly protocol_version: {
@ -76,13 +84,6 @@ export interface BlockResponse {
readonly block_id: BlockId;
readonly block: Block;
}
export interface AuthAccountsResponse {
readonly height: string;
readonly result: {
readonly type: "cosmos-sdk/Account";
readonly value: CosmosSdkAccount;
};
}
export interface TxsResponse {
readonly height: string;
readonly txhash: string;
@ -125,56 +126,4 @@ export interface PostTxsResponse {
/** The gas used by the execution */
readonly gas_used?: string;
}
/** A reponse from the /txs/encode endpoint */
export interface EncodeTxResponse {
/** base64-encoded amino-binary encoded representation */
readonly tx: string;
}
/**
* The mode used to send transaction
*
* @see https://cosmos.network/rpc/#/Transactions/post_txs
*/
export declare enum BroadcastMode {
/** Return after tx commit */
Block = "block",
/** Return afer CheckTx */
Sync = "sync",
/** Return right away */
Async = "async",
}
export declare class RestClient {
private readonly client;
private readonly broadcastMode;
/**
* Creates a new client to interact with a Cosmos SDK light client daemon.
* This class tries to be a direct mapping onto the API. Some basic decoding and normalizatin is done
* but things like caching are done at a higher level.
*
* When building apps, you should not need to use this class directly. If you do, this indicates a missing feature
* in higher level components. Feel free to raise an issue in this case.
*
* @param apiUrl The URL of a Cosmos SDK light client daemon API (sometimes called REST server or REST API)
* @param broadcastMode Defines at which point of the transaction processing the postTx method (i.e. transaction broadcasting) returns
*/
constructor(apiUrl: string, broadcastMode?: BroadcastMode);
get(path: string): Promise<any>;
post(path: string, params: any): Promise<any>;
authAccounts(address: string): Promise<AuthAccountsResponse>;
blocksLatest(): Promise<BlockResponse>;
blocks(height: number): Promise<BlockResponse>;
nodeInfo(): Promise<NodeInfoResponse>;
txById(id: string): Promise<TxsResponse>;
txsQuery(query: string): Promise<SearchTxsResponse>;
/** returns the amino-encoding of the transaction performed by the server */
encodeTx(tx: CosmosSdkTx): Promise<EncodeTxResponse>;
/**
* Broadcasts a signed transaction to into the transaction pool.
* Depending on the RestClient's broadcast mode, this might or might
* wait for checkTx or deliverTx to be executed before returning.
*
* @param tx a signed transaction as StdTx (i.e. not wrapped in type/value container)
*/
postTx(tx: StdTx): Promise<PostTxsResponse>;
}
export {};

12
packages/sdk38/types/lcdapi/index.d.ts vendored Normal file
View File

@ -0,0 +1,12 @@
export { AuthExtension, AuthAccountsResponse, setupAuthExtension } from "./auth";
export { setupSupplyExtension, SupplyExtension, TotalSupplyAllResponse, TotalSupplyResponse } from "./supply";
export {
BlockResponse,
BroadcastMode,
EncodeTxResponse,
PostTxsResponse,
NodeInfoResponse,
SearchTxsResponse,
TxsResponse,
} from "./base";
export { LcdApiArray, LcdClient, normalizeLcdApiArray } from "./lcdclient";

View File

@ -0,0 +1,164 @@
import { CosmosSdkTx, StdTx } from "../types";
import {
BlockResponse,
BroadcastMode,
EncodeTxResponse,
NodeInfoResponse,
PostTxsResponse,
SearchTxsResponse,
TxsResponse,
} from "./base";
/** Unfortunately, Cosmos SDK encodes empty arrays as null */
export declare type LcdApiArray<T> = readonly T[] | null;
export declare function normalizeLcdApiArray<T>(backend: LcdApiArray<T>): readonly T[];
declare type LcdExtensionSetup<P> = (base: LcdClient) => P;
export interface LcdClientBaseOptions {
readonly apiUrl: string;
readonly broadcastMode?: BroadcastMode;
}
/**
* A client to the LCD's (light client daemon) API.
* This light client connects to Tendermint (i.e. the chain), encodes/decodes Amino data for us and provides a convenient JSON interface.
*
* This _JSON over HTTP_ API is sometimes referred to as "REST" or "RPC", which are both misleading terms
* for the same thing.
*
* Please note that the client to the LCD can not verify light client proofs. When using this,
* you need to trust the API provider as well as the network connection between client and API.
*
* @see https://cosmos.network/rpc
*/
export declare class LcdClient {
/** Constructs an LCD client with 0 extensions */
static withExtensions(options: LcdClientBaseOptions): LcdClient;
/** Constructs an LCD client with 1 extension */
static withExtensions<A extends object>(
options: LcdClientBaseOptions,
setupExtensionA: LcdExtensionSetup<A>,
): LcdClient & A;
/** Constructs an LCD client with 2 extensions */
static withExtensions<A extends object, B extends object>(
options: LcdClientBaseOptions,
setupExtensionA: LcdExtensionSetup<A>,
setupExtensionB: LcdExtensionSetup<B>,
): LcdClient & A & B;
/** Constructs an LCD client with 3 extensions */
static withExtensions<A extends object, B extends object, C extends object>(
options: LcdClientBaseOptions,
setupExtensionA: LcdExtensionSetup<A>,
setupExtensionB: LcdExtensionSetup<B>,
setupExtensionC: LcdExtensionSetup<C>,
): LcdClient & A & B & C;
/** Constructs an LCD client with 4 extensions */
static withExtensions<A extends object, B extends object, C extends object, D extends object>(
options: LcdClientBaseOptions,
setupExtensionA: LcdExtensionSetup<A>,
setupExtensionB: LcdExtensionSetup<B>,
setupExtensionC: LcdExtensionSetup<C>,
setupExtensionD: LcdExtensionSetup<D>,
): LcdClient & A & B & C & D;
/** Constructs an LCD client with 5 extensions */
static withExtensions<
A extends object,
B extends object,
C extends object,
D extends object,
E extends object
>(
options: LcdClientBaseOptions,
setupExtensionA: LcdExtensionSetup<A>,
setupExtensionB: LcdExtensionSetup<B>,
setupExtensionC: LcdExtensionSetup<C>,
setupExtensionD: LcdExtensionSetup<D>,
setupExtensionE: LcdExtensionSetup<E>,
): LcdClient & A & B & C & D & E;
/** Constructs an LCD client with 6 extensions */
static withExtensions<
A extends object,
B extends object,
C extends object,
D extends object,
E extends object,
F extends object
>(
options: LcdClientBaseOptions,
setupExtensionA: LcdExtensionSetup<A>,
setupExtensionB: LcdExtensionSetup<B>,
setupExtensionC: LcdExtensionSetup<C>,
setupExtensionD: LcdExtensionSetup<D>,
setupExtensionE: LcdExtensionSetup<E>,
setupExtensionF: LcdExtensionSetup<F>,
): LcdClient & A & B & C & D & E & F;
/** Constructs an LCD client with 7 extensions */
static withExtensions<
A extends object,
B extends object,
C extends object,
D extends object,
E extends object,
F extends object,
G extends object
>(
options: LcdClientBaseOptions,
setupExtensionA: LcdExtensionSetup<A>,
setupExtensionB: LcdExtensionSetup<B>,
setupExtensionC: LcdExtensionSetup<C>,
setupExtensionD: LcdExtensionSetup<D>,
setupExtensionE: LcdExtensionSetup<E>,
setupExtensionF: LcdExtensionSetup<F>,
setupExtensionG: LcdExtensionSetup<G>,
): LcdClient & A & B & C & D & E & F & G;
/** Constructs an LCD client with 8 extensions */
static withExtensions<
A extends object,
B extends object,
C extends object,
D extends object,
E extends object,
F extends object,
G extends object,
H extends object
>(
options: LcdClientBaseOptions,
setupExtensionA: LcdExtensionSetup<A>,
setupExtensionB: LcdExtensionSetup<B>,
setupExtensionC: LcdExtensionSetup<C>,
setupExtensionD: LcdExtensionSetup<D>,
setupExtensionE: LcdExtensionSetup<E>,
setupExtensionF: LcdExtensionSetup<F>,
setupExtensionG: LcdExtensionSetup<G>,
setupExtensionH: LcdExtensionSetup<H>,
): LcdClient & A & B & C & D & E & F & G & H;
private readonly client;
private readonly broadcastMode;
/**
* Creates a new client to interact with a Cosmos SDK light client daemon.
* This class tries to be a direct mapping onto the API. Some basic decoding and normalizatin is done
* but things like caching are done at a higher level.
*
* When building apps, you should not need to use this class directly. If you do, this indicates a missing feature
* in higher level components. Feel free to raise an issue in this case.
*
* @param apiUrl The URL of a Cosmos SDK light client daemon API (sometimes called REST server or REST API)
* @param broadcastMode Defines at which point of the transaction processing the postTx method (i.e. transaction broadcasting) returns
*/
constructor(apiUrl: string, broadcastMode?: BroadcastMode);
get(path: string): Promise<any>;
post(path: string, params: any): Promise<any>;
blocksLatest(): Promise<BlockResponse>;
blocks(height: number): Promise<BlockResponse>;
nodeInfo(): Promise<NodeInfoResponse>;
txById(id: string): Promise<TxsResponse>;
txsQuery(query: string): Promise<SearchTxsResponse>;
/** returns the amino-encoding of the transaction performed by the server */
encodeTx(tx: CosmosSdkTx): Promise<EncodeTxResponse>;
/**
* Broadcasts a signed transaction to the transaction pool.
* Depending on the client's broadcast mode, this might or might
* wait for checkTx or deliverTx to be executed before returning.
*
* @param tx a signed transaction as StdTx (i.e. not wrapped in type/value container)
*/
postTx(tx: StdTx): Promise<PostTxsResponse>;
}
export {};

18
packages/sdk38/types/lcdapi/supply.d.ts vendored Normal file
View File

@ -0,0 +1,18 @@
import { Coin } from "../coins";
import { LcdApiArray, LcdClient } from "./lcdclient";
export interface TotalSupplyAllResponse {
readonly height: string;
readonly result: LcdApiArray<Coin>;
}
export interface TotalSupplyResponse {
readonly height: string;
/** The amount */
readonly result: string;
}
export interface SupplyExtension {
readonly supply: {
readonly totalAll: () => Promise<TotalSupplyAllResponse>;
readonly total: (denom: string) => Promise<TotalSupplyResponse>;
};
}
export declare function setupSupplyExtension(base: LcdClient): SupplyExtension;

View File

@ -1,6 +1,6 @@
import { Coin } from "./coins";
import { Account, CosmosClient, GetNonceResult, PostTxResult } from "./cosmosclient";
import { BroadcastMode } from "./restclient";
import { BroadcastMode } from "./lcdapi";
import { StdFee, StdSignature } from "./types";
export interface SigningCallback {
(signBytes: Uint8Array): Promise<StdSignature>;