diff --git a/packages/cli/examples/local_faucet.ts b/packages/cli/examples/local_faucet.ts index 77a447b6..687e3512 100644 --- a/packages/cli/examples/local_faucet.ts +++ b/packages/cli/examples/local_faucet.ts @@ -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); diff --git a/packages/cli/src/cli.ts b/packages/cli/src/cli.ts index 3ac27c5c..d5ead030 100644 --- a/packages/cli/src/cli.ts +++ b/packages/cli/src/cli.ts @@ -96,10 +96,10 @@ export function main(originalArgs: readonly string[]): void { "Msg", "MsgDelegate", "MsgSend", + "LcdClient", "Pen", "PubKey", "pubkeyToAddress", - "RestClient", "Secp256k1Pen", "SigningCosmosClient", "StdFee", diff --git a/packages/cosmwasm/package.json b/packages/cosmwasm/package.json index dac7b17b..e5b2ab65 100644 --- a/packages/cosmwasm/package.json +++ b/packages/cosmwasm/package.json @@ -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", diff --git a/packages/cosmwasm/src/cosmwasmclient.searchtx.spec.ts b/packages/cosmwasm/src/cosmwasmclient.searchtx.spec.ts index 13b44308..e114e219 100644 --- a/packages/cosmwasm/src/cosmwasmclient.searchtx.spec.ts +++ b/packages/cosmwasm/src/cosmwasmclient.searchtx.spec.ts @@ -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, diff --git a/packages/cosmwasm/src/cosmwasmclient.spec.ts b/packages/cosmwasm/src/cosmwasmclient.spec.ts index 0d176f63..3928a075 100644 --- a/packages/cosmwasm/src/cosmwasmclient.spec.ts +++ b/packages/cosmwasm/src/cosmwasmclient.spec.ts @@ -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 diff --git a/packages/cosmwasm/src/cosmwasmclient.ts b/packages/cosmwasm/src/cosmwasmclient.ts index 4d7d4a4c..66bd2286 100644 --- a/packages/cosmwasm/src/cosmwasmclient.ts +++ b/packages/cosmwasm/src/cosmwasmclient.ts @@ -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 { 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 { 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 { // 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 { - 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 { 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 { - 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 { - 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 { - 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 { - 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 { 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 { // 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( diff --git a/packages/cosmwasm/src/index.ts b/packages/cosmwasm/src/index.ts index 3c554430..8613a66a 100644 --- a/packages/cosmwasm/src/index.ts +++ b/packages/cosmwasm/src/index.ts @@ -1,7 +1,7 @@ import * as logs from "./logs"; export { logs }; -export { RestClient } from "./restclient"; +export { setupWasmExtension, WasmExtension } from "./lcdapi/wasm"; export { Account, Block, diff --git a/packages/cosmwasm/src/restclient.spec.ts b/packages/cosmwasm/src/lcdapi/wasm.spec.ts similarity index 85% rename from packages/cosmwasm/src/restclient.spec.ts rename to packages/cosmwasm/src/lcdapi/wasm.spec.ts index 3b45542a..08dac3cf 100644 --- a/packages/cosmwasm/src/restclient.spec.ts +++ b/packages/cosmwasm/src/lcdapi/wasm.spec.ts @@ -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 { @@ -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"), ); diff --git a/packages/cosmwasm/src/lcdapi/wasm.ts b/packages/cosmwasm/src/lcdapi/wasm.ts new file mode 100644 index 00000000..f0b70f65 --- /dev/null +++ b/packages/cosmwasm/src/lcdapi/wasm.ts @@ -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 = WasmSuccess | WasmError; + +interface WasmSuccess { + 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(resp: WasmResponse): resp is WasmError { + return (resp as WasmError).error !== undefined; +} + +function unwrapWasmResponse(response: WasmResponse): 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; + + /** + * Downloads the original wasm bytecode by code ID. + * + * Throws an error if no code with this id + */ + readonly getCode: (id: number) => Promise; + + readonly listContractsByCodeId: (id: number) => Promise; + + /** + * Returns null when contract was not found at this address. + */ + readonly getContractInfo: (address: string) => Promise; + + /** + * Returns all contract state. + * This is an empty array if no such contract, or contract has no data. + */ + readonly getAllContractState: (address: string) => Promise; + + /** + * 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; + + /** + * 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; + }; +} + +export function setupWasmExtension(base: LcdClient): WasmExtension { + return { + wasm: { + listCodeInfo: async () => { + const path = `/wasm/code`; + const responseData = (await base.get(path)) as WasmResponse>; + return normalizeLcdApiArray(unwrapWasmResponse(responseData)); + }, + getCode: async (id: number) => { + const path = `/wasm/code/${id}`; + const responseData = (await base.get(path)) as WasmResponse; + return unwrapWasmResponse(responseData); + }, + listContractsByCodeId: async (id: number) => { + const path = `/wasm/code/${id}/contracts`; + const responseData = (await base.get(path)) as WasmResponse>; + return normalizeLcdApiArray(unwrapWasmResponse(responseData)); + }, + getContractInfo: async (address: string) => { + const path = `/wasm/contract/${address}`; + const response = (await base.get(path)) as WasmResponse; + return unwrapWasmResponse(response); + }, + getAllContractState: async (address: string) => { + const path = `/wasm/contract/${address}/state`; + const responseData = (await base.get(path)) as WasmResponse>; + 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; + 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; + 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))); + }, + }, + }; +} diff --git a/packages/cosmwasm/src/restclient.ts b/packages/cosmwasm/src/restclient.ts deleted file mode 100644 index a915f565..00000000 --- a/packages/cosmwasm/src/restclient.ts +++ /dev/null @@ -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 = WasmSuccess | WasmError; - -interface WasmSuccess { - 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 = readonly T[] | null; - -function normalizeArray(backend: CosmosSdkArray): readonly T[] { - return backend || []; -} - -function isWasmError(resp: WasmResponse): resp is WasmError { - return (resp as WasmError).error !== undefined; -} - -function unwrapWasmResponse(response: WasmResponse): 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 { - const path = `/wasm/code`; - const responseData = (await this.get(path)) as WasmResponse>; - 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 { - const path = `/wasm/code/${id}`; - const responseData = (await this.get(path)) as WasmResponse; - return unwrapWasmResponse(responseData); - } - - public async listContractsByCodeId(id: number): Promise { - const path = `/wasm/code/${id}/contracts`; - const responseData = (await this.get(path)) as WasmResponse>; - return normalizeArray(unwrapWasmResponse(responseData)); - } - - /** - * Returns null when contract was not found at this address. - */ - public async getContractInfo(address: string): Promise { - const path = `/wasm/contract/${address}`; - const response = (await this.get(path)) as WasmResponse; - 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 { - const path = `/wasm/contract/${address}/state`; - const responseData = (await this.get(path)) as WasmResponse>; - 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 { - const hexKey = toHex(key); - const path = `/wasm/contract/${address}/raw/${hexKey}?encoding=hex`; - const responseData = (await this.get(path)) as WasmResponse; - 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 { - const encoded = toHex(toUtf8(JSON.stringify(query))); - const path = `/wasm/contract/${address}/smart/${encoded}?encoding=hex`; - const responseData = (await this.get(path)) as WasmResponse; - 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))); - } -} diff --git a/packages/cosmwasm/src/signingcosmwasmclient.spec.ts b/packages/cosmwasm/src/signingcosmwasmclient.spec.ts index b828edde..b1e71f07 100644 --- a/packages/cosmwasm/src/signingcosmwasmclient.spec.ts +++ b/packages/cosmwasm/src/signingcosmwasmclient.spec.ts @@ -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([]); }); }); diff --git a/packages/cosmwasm/src/signingcosmwasmclient.ts b/packages/cosmwasm/src/signingcosmwasmclient.ts index cae59657..e713e82f 100644 --- a/packages/cosmwasm/src/signingcosmwasmclient.ts +++ b/packages/cosmwasm/src/signingcosmwasmclient.ts @@ -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; } diff --git a/packages/cosmwasm/src/testutils.spec.ts b/packages/cosmwasm/src/testutils.spec.ts index e2fb52f6..5cfe24a4 100644 --- a/packages/cosmwasm/src/testutils.spec.ts +++ b/packages/cosmwasm/src/testutils.spec.ts @@ -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(elements: ArrayLike): 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], + }; +} diff --git a/packages/cosmwasm/types/cosmwasmclient.d.ts b/packages/cosmwasm/types/cosmwasmclient.d.ts index 87e5360c..c72cdc17 100644 --- a/packages/cosmwasm/types/cosmwasmclient.d.ts +++ b/packages/cosmwasm/types/cosmwasmclient.d.ts @@ -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; diff --git a/packages/cosmwasm/types/index.d.ts b/packages/cosmwasm/types/index.d.ts index 0cd933f0..6a9a884b 100644 --- a/packages/cosmwasm/types/index.d.ts +++ b/packages/cosmwasm/types/index.d.ts @@ -1,6 +1,6 @@ import * as logs from "./logs"; export { logs }; -export { RestClient } from "./restclient"; +export { setupWasmExtension, WasmExtension } from "./lcdapi/wasm"; export { Account, Block, diff --git a/packages/cosmwasm/types/lcdapi/wasm.d.ts b/packages/cosmwasm/types/lcdapi/wasm.d.ts new file mode 100644 index 00000000..4f3b06a1 --- /dev/null +++ b/packages/cosmwasm/types/lcdapi/wasm.d.ts @@ -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; + /** + * Downloads the original wasm bytecode by code ID. + * + * Throws an error if no code with this id + */ + readonly getCode: (id: number) => Promise; + readonly listContractsByCodeId: (id: number) => Promise; + /** + * Returns null when contract was not found at this address. + */ + readonly getContractInfo: (address: string) => Promise; + /** + * Returns all contract state. + * This is an empty array if no such contract, or contract has no data. + */ + readonly getAllContractState: (address: string) => Promise; + /** + * 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; + /** + * 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; + }; +} +export declare function setupWasmExtension(base: LcdClient): WasmExtension; diff --git a/packages/cosmwasm/types/restclient.d.ts b/packages/cosmwasm/types/restclient.d.ts deleted file mode 100644 index 2ca6f5e2..00000000 --- a/packages/cosmwasm/types/restclient.d.ts +++ /dev/null @@ -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; - getCode(id: number): Promise; - listContractsByCodeId(id: number): Promise; - /** - * Returns null when contract was not found at this address. - */ - getContractInfo(address: string): Promise; - getAllContractState(address: string): Promise; - queryContractRaw(address: string, key: Uint8Array): Promise; - /** - * 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; -} diff --git a/packages/cosmwasm/types/signingcosmwasmclient.d.ts b/packages/cosmwasm/types/signingcosmwasmclient.d.ts index b86bebeb..2a3e17ea 100644 --- a/packages/cosmwasm/types/signingcosmwasmclient.d.ts +++ b/packages/cosmwasm/types/signingcosmwasmclient.d.ts @@ -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 { diff --git a/packages/sdk38/package.json b/packages/sdk38/package.json index 5d2b6087..bd5a1110 100644 --- a/packages/sdk38/package.json +++ b/packages/sdk38/package.json @@ -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", diff --git a/packages/sdk38/src/cosmosclient.searchtx.spec.ts b/packages/sdk38/src/cosmosclient.searchtx.spec.ts index 9c42f8fb..cb6ca4de 100644 --- a/packages/sdk38/src/cosmosclient.searchtx.spec.ts +++ b/packages/sdk38/src/cosmosclient.searchtx.spec.ts @@ -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, diff --git a/packages/sdk38/src/cosmosclient.spec.ts b/packages/sdk38/src/cosmosclient.spec.ts index cdea2316..64d9806d 100644 --- a/packages/sdk38/src/cosmosclient.spec.ts +++ b/packages/sdk38/src/cosmosclient.spec.ts @@ -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); diff --git a/packages/sdk38/src/cosmosclient.ts b/packages/sdk38/src/cosmosclient.ts index 077e354c..0d977502 100644 --- a/packages/sdk38/src/cosmosclient.ts +++ b/packages/sdk38/src/cosmosclient.ts @@ -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 { 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 { 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 { // 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 { - 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 { 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 { - 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 { // 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( diff --git a/packages/sdk38/src/index.ts b/packages/sdk38/src/index.ts index 7735fa63..7c7d6b96 100644 --- a/packages/sdk38/src/index.ts +++ b/packages/sdk38/src/index.ts @@ -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"; diff --git a/packages/sdk38/src/lcdapi/auth.spec.ts b/packages/sdk38/src/lcdapi/auth.spec.ts new file mode 100644 index 00000000..2cd2ed45 --- /dev/null +++ b/packages/sdk38/src/lcdapi/auth.spec.ts @@ -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: "" }), + }); + }); +}); diff --git a/packages/sdk38/src/lcdapi/auth.ts b/packages/sdk38/src/lcdapi/auth.ts new file mode 100644 index 00000000..d9066733 --- /dev/null +++ b/packages/sdk38/src/lcdapi/auth.ts @@ -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; + }; +} + +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; + }, + }, + }; +} diff --git a/packages/sdk38/src/lcdapi/base.ts b/packages/sdk38/src/lcdapi/base.ts new file mode 100644 index 00000000..9b13b20d --- /dev/null +++ b/packages/sdk38/src/lcdapi/base.ts @@ -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; +} diff --git a/packages/sdk38/src/lcdapi/index.ts b/packages/sdk38/src/lcdapi/index.ts new file mode 100644 index 00000000..b2a25ab0 --- /dev/null +++ b/packages/sdk38/src/lcdapi/index.ts @@ -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"; diff --git a/packages/sdk38/src/restclient.spec.ts b/packages/sdk38/src/lcdapi/lcdclient.spec.ts similarity index 70% rename from packages/sdk38/src/restclient.spec.ts rename to packages/sdk38/src/lcdapi/lcdclient.spec.ts index 207c3d98..627075e1 100644 --- a/packages/sdk38/src/restclient.spec.ts +++ b/packages/sdk38/src/lcdapi/lcdclient.spec.ts @@ -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 = WasmSuccess | WasmError; + + interface WasmSuccess { + readonly height: string; + readonly result: T; + } + + interface WasmError { + readonly error: string; + } + + function isWasmError(resp: WasmResponse): resp is WasmError { + return (resp as WasmError).error !== undefined; + } + + function unwrapWasmResponse(response: WasmResponse): T { + if (isWasmError(response)) { + throw new Error(response.error); + } + return response.result; + } + + interface WasmExtension { + wasm: { + listCodeInfo: () => Promise; + }; + } + + function setupWasmExtension(base: LcdClient): WasmExtension { + return { + wasm: { + listCodeInfo: async (): Promise => { + const path = `/wasm/code`; + const responseData = (await base.get(path)) as WasmResponse>; + 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; + } + + // eslint-disable-next-line @typescript-eslint/explicit-function-return-type + function setupSupplyExtension(base: LcdClient) { + return { + supply: { + totalAll: async (): Promise => { + 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); diff --git a/packages/sdk38/src/lcdapi/lcdclient.ts b/packages/sdk38/src/lcdapi/lcdclient.ts new file mode 100644 index 00000000..c2233651 --- /dev/null +++ b/packages/sdk38/src/lcdapi/lcdclient.ts @@ -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 = readonly T[] | null; + +export function normalizeLcdApiArray(backend: LcdApiArray): readonly T[] { + return backend || []; +} + +type LcdExtensionSetup

= (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( + options: LcdClientBaseOptions, + setupExtensionA: LcdExtensionSetup, + ): LcdClient & A; + + /** Constructs an LCD client with 2 extensions */ + public static withExtensions( + options: LcdClientBaseOptions, + setupExtensionA: LcdExtensionSetup, + setupExtensionB: LcdExtensionSetup, + ): LcdClient & A & B; + + /** Constructs an LCD client with 3 extensions */ + public static withExtensions( + options: LcdClientBaseOptions, + setupExtensionA: LcdExtensionSetup, + setupExtensionB: LcdExtensionSetup, + setupExtensionC: LcdExtensionSetup, + ): LcdClient & A & B & C; + + /** Constructs an LCD client with 4 extensions */ + public static withExtensions( + options: LcdClientBaseOptions, + setupExtensionA: LcdExtensionSetup, + setupExtensionB: LcdExtensionSetup, + setupExtensionC: LcdExtensionSetup, + setupExtensionD: LcdExtensionSetup, + ): 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, + setupExtensionB: LcdExtensionSetup, + setupExtensionC: LcdExtensionSetup, + setupExtensionD: LcdExtensionSetup, + setupExtensionE: LcdExtensionSetup, + ): 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, + setupExtensionB: LcdExtensionSetup, + setupExtensionC: LcdExtensionSetup, + setupExtensionD: LcdExtensionSetup, + setupExtensionE: LcdExtensionSetup, + setupExtensionF: LcdExtensionSetup, + ): 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, + setupExtensionB: LcdExtensionSetup, + setupExtensionC: LcdExtensionSetup, + setupExtensionD: LcdExtensionSetup, + setupExtensionE: LcdExtensionSetup, + setupExtensionF: LcdExtensionSetup, + setupExtensionG: LcdExtensionSetup, + ): 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, + setupExtensionB: LcdExtensionSetup, + setupExtensionC: LcdExtensionSetup, + setupExtensionD: LcdExtensionSetup, + setupExtensionE: LcdExtensionSetup, + setupExtensionF: LcdExtensionSetup, + setupExtensionG: LcdExtensionSetup, + setupExtensionH: LcdExtensionSetup, + ): LcdClient & A & B & C & D & E & F & G & H; + + public static withExtensions( + options: LcdClientBaseOptions, + ...extensionSetups: Array> + ): 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 { + 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 { + 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 { + 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 { + 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 { + 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 { + 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 { + 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 { + 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 { + 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; + } +} diff --git a/packages/sdk38/src/lcdapi/supply.spec.ts b/packages/sdk38/src/lcdapi/supply.spec.ts new file mode 100644 index 00000000..ca95f5d8 --- /dev/null +++ b/packages/sdk38/src/lcdapi/supply.spec.ts @@ -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]+$/), + }); + }); + }); +}); diff --git a/packages/sdk38/src/lcdapi/supply.ts b/packages/sdk38/src/lcdapi/supply.ts new file mode 100644 index 00000000..7ddfc883 --- /dev/null +++ b/packages/sdk38/src/lcdapi/supply.ts @@ -0,0 +1,33 @@ +import { Coin } from "../coins"; +import { LcdApiArray, LcdClient } from "./lcdclient"; + +export interface TotalSupplyAllResponse { + readonly height: string; + readonly result: LcdApiArray; +} + +export interface TotalSupplyResponse { + readonly height: string; + /** The amount */ + readonly result: string; +} + +export interface SupplyExtension { + readonly supply: { + readonly totalAll: () => Promise; + readonly total: (denom: string) => Promise; + }; +} + +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}`); + }, + }, + }; +} diff --git a/packages/sdk38/src/restclient.ts b/packages/sdk38/src/restclient.ts deleted file mode 100644 index 67edfb99..00000000 --- a/packages/sdk38/src/restclient.ts +++ /dev/null @@ -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 { - 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 { - 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 { - 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 { - 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 { - 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 { - 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 { - 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 { - 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 { - 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 { - 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; - } -} diff --git a/packages/sdk38/src/signingcosmosclient.spec.ts b/packages/sdk38/src/signingcosmosclient.spec.ts index 258c4f76..ece3473c 100644 --- a/packages/sdk38/src/signingcosmosclient.spec.ts +++ b/packages/sdk38/src/signingcosmosclient.spec.ts @@ -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); diff --git a/packages/sdk38/src/signingcosmosclient.ts b/packages/sdk38/src/signingcosmosclient.ts index 4768bf60..74f23fef 100644 --- a/packages/sdk38/src/signingcosmosclient.ts +++ b/packages/sdk38/src/signingcosmosclient.ts @@ -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 { diff --git a/packages/sdk38/src/testutils.spec.ts b/packages/sdk38/src/testutils.spec.ts index b04b1de5..e61c6927 100644 --- a/packages/sdk38/src/testutils.spec.ts +++ b/packages/sdk38/src/testutils.spec.ts @@ -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(elements: ArrayLike): 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], + }; +} diff --git a/packages/sdk38/types/cosmosclient.d.ts b/packages/sdk38/types/cosmosclient.d.ts index b62fb2c6..5ce6b60c 100644 --- a/packages/sdk38/types/cosmosclient.d.ts +++ b/packages/sdk38/types/cosmosclient.d.ts @@ -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; diff --git a/packages/sdk38/types/index.d.ts b/packages/sdk38/types/index.d.ts index 633dc24b..ea017984 100644 --- a/packages/sdk38/types/index.d.ts +++ b/packages/sdk38/types/index.d.ts @@ -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"; diff --git a/packages/sdk38/types/lcdapi/auth.d.ts b/packages/sdk38/types/lcdapi/auth.d.ts new file mode 100644 index 00000000..95f404d3 --- /dev/null +++ b/packages/sdk38/types/lcdapi/auth.d.ts @@ -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; + }; +} +export declare function setupAuthExtension(base: LcdClient): AuthExtension; diff --git a/packages/sdk38/types/restclient.d.ts b/packages/sdk38/types/lcdapi/base.d.ts similarity index 59% rename from packages/sdk38/types/restclient.d.ts rename to packages/sdk38/types/lcdapi/base.d.ts index b1134d30..8771acf4 100644 --- a/packages/sdk38/types/restclient.d.ts +++ b/packages/sdk38/types/lcdapi/base.d.ts @@ -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; - post(path: string, params: any): Promise; - authAccounts(address: string): Promise; - blocksLatest(): Promise; - blocks(height: number): Promise; - nodeInfo(): Promise; - txById(id: string): Promise; - txsQuery(query: string): Promise; - /** returns the amino-encoding of the transaction performed by the server */ - encodeTx(tx: CosmosSdkTx): Promise; - /** - * 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; -} export {}; diff --git a/packages/sdk38/types/lcdapi/index.d.ts b/packages/sdk38/types/lcdapi/index.d.ts new file mode 100644 index 00000000..ca9ac056 --- /dev/null +++ b/packages/sdk38/types/lcdapi/index.d.ts @@ -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"; diff --git a/packages/sdk38/types/lcdapi/lcdclient.d.ts b/packages/sdk38/types/lcdapi/lcdclient.d.ts new file mode 100644 index 00000000..ce67cc38 --- /dev/null +++ b/packages/sdk38/types/lcdapi/lcdclient.d.ts @@ -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 = readonly T[] | null; +export declare function normalizeLcdApiArray(backend: LcdApiArray): readonly T[]; +declare type LcdExtensionSetup

= (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( + options: LcdClientBaseOptions, + setupExtensionA: LcdExtensionSetup, + ): LcdClient & A; + /** Constructs an LCD client with 2 extensions */ + static withExtensions( + options: LcdClientBaseOptions, + setupExtensionA: LcdExtensionSetup, + setupExtensionB: LcdExtensionSetup, + ): LcdClient & A & B; + /** Constructs an LCD client with 3 extensions */ + static withExtensions( + options: LcdClientBaseOptions, + setupExtensionA: LcdExtensionSetup, + setupExtensionB: LcdExtensionSetup, + setupExtensionC: LcdExtensionSetup, + ): LcdClient & A & B & C; + /** Constructs an LCD client with 4 extensions */ + static withExtensions( + options: LcdClientBaseOptions, + setupExtensionA: LcdExtensionSetup, + setupExtensionB: LcdExtensionSetup, + setupExtensionC: LcdExtensionSetup, + setupExtensionD: LcdExtensionSetup, + ): 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, + setupExtensionB: LcdExtensionSetup, + setupExtensionC: LcdExtensionSetup, + setupExtensionD: LcdExtensionSetup, + setupExtensionE: LcdExtensionSetup, + ): 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, + setupExtensionB: LcdExtensionSetup, + setupExtensionC: LcdExtensionSetup, + setupExtensionD: LcdExtensionSetup, + setupExtensionE: LcdExtensionSetup, + setupExtensionF: LcdExtensionSetup, + ): 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, + setupExtensionB: LcdExtensionSetup, + setupExtensionC: LcdExtensionSetup, + setupExtensionD: LcdExtensionSetup, + setupExtensionE: LcdExtensionSetup, + setupExtensionF: LcdExtensionSetup, + setupExtensionG: LcdExtensionSetup, + ): 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, + setupExtensionB: LcdExtensionSetup, + setupExtensionC: LcdExtensionSetup, + setupExtensionD: LcdExtensionSetup, + setupExtensionE: LcdExtensionSetup, + setupExtensionF: LcdExtensionSetup, + setupExtensionG: LcdExtensionSetup, + setupExtensionH: LcdExtensionSetup, + ): 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; + post(path: string, params: any): Promise; + blocksLatest(): Promise; + blocks(height: number): Promise; + nodeInfo(): Promise; + txById(id: string): Promise; + txsQuery(query: string): Promise; + /** returns the amino-encoding of the transaction performed by the server */ + encodeTx(tx: CosmosSdkTx): Promise; + /** + * 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; +} +export {}; diff --git a/packages/sdk38/types/lcdapi/supply.d.ts b/packages/sdk38/types/lcdapi/supply.d.ts new file mode 100644 index 00000000..5d9fe774 --- /dev/null +++ b/packages/sdk38/types/lcdapi/supply.d.ts @@ -0,0 +1,18 @@ +import { Coin } from "../coins"; +import { LcdApiArray, LcdClient } from "./lcdclient"; +export interface TotalSupplyAllResponse { + readonly height: string; + readonly result: LcdApiArray; +} +export interface TotalSupplyResponse { + readonly height: string; + /** The amount */ + readonly result: string; +} +export interface SupplyExtension { + readonly supply: { + readonly totalAll: () => Promise; + readonly total: (denom: string) => Promise; + }; +} +export declare function setupSupplyExtension(base: LcdClient): SupplyExtension; diff --git a/packages/sdk38/types/signingcosmosclient.d.ts b/packages/sdk38/types/signingcosmosclient.d.ts index d9ac523a..a733b8f6 100644 --- a/packages/sdk38/types/signingcosmosclient.d.ts +++ b/packages/sdk38/types/signingcosmosclient.d.ts @@ -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;