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/src/index.ts b/packages/cosmwasm/src/index.ts index 993c143e..8613a66a 100644 --- a/packages/cosmwasm/src/index.ts +++ b/packages/cosmwasm/src/index.ts @@ -2,7 +2,6 @@ import * as logs from "./logs"; export { logs }; export { setupWasmExtension, WasmExtension } from "./lcdapi/wasm"; -export { RestClient } from "./restclient"; export { Account, Block, diff --git a/packages/cosmwasm/src/restclient.spec.ts b/packages/cosmwasm/src/restclient.spec.ts deleted file mode 100644 index 7afcd29a..00000000 --- a/packages/cosmwasm/src/restclient.spec.ts +++ /dev/null @@ -1,494 +0,0 @@ -/* eslint-disable @typescript-eslint/camelcase */ -import { Sha256 } from "@cosmjs/crypto"; -import { Bech32, fromAscii, fromBase64, fromHex, toAscii, toBase64, toHex } from "@cosmjs/encoding"; -import { Coin, coin, coins, makeSignBytes, Pen, PostTxsResponse, Secp256k1Pen, StdFee } from "@cosmjs/sdk38"; -import { assert } from "@cosmjs/utils"; - -import { findAttribute, parseLogs } from "./logs"; -import { - isMsgInstantiateContract, - isMsgStoreCode, - MsgExecuteContract, - MsgInstantiateContract, - MsgStoreCode, -} from "./msgs"; -import { RestClient } from "./restclient"; -import { - alice, - bech32AddressMatcher, - ContractUploadInstructions, - deployedErc20, - fromOneElementArray, - getHackatom, - makeRandomAddress, - makeSignedTx, - pendingWithoutWasmd, - wasmd, - wasmdEnabled, -} from "./testutils.spec"; - -async function uploadContract( - client: RestClient, - pen: Pen, - contract: ContractUploadInstructions, -): Promise { - const memo = "My first contract on chain"; - const theMsg: MsgStoreCode = { - type: "wasm/store-code", - value: { - sender: alice.address0, - wasm_byte_code: toBase64(contract.data), - source: contract.source || "", - builder: contract.builder || "", - }, - }; - const fee: StdFee = { - amount: [ - { - amount: "5000000", - denom: "ucosm", - }, - ], - gas: "89000000", - }; - - const { account_number, sequence } = (await client.authAccounts(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); -} - -async function instantiateContract( - client: RestClient, - pen: Pen, - codeId: number, - beneficiaryAddress: string, - transferAmount?: readonly Coin[], -): Promise { - const memo = "Create an escrow instance"; - const theMsg: MsgInstantiateContract = { - type: "wasm/instantiate", - value: { - sender: alice.address0, - code_id: codeId.toString(), - label: "my escrow", - init_msg: { - verifier: alice.address0, - beneficiary: beneficiaryAddress, - }, - init_funds: transferAmount || [], - }, - }; - const fee: StdFee = { - amount: [ - { - amount: "5000000", - denom: "ucosm", - }, - ], - gas: "89000000", - }; - - const { account_number, sequence } = (await client.authAccounts(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); -} - -async function executeContract( - client: RestClient, - pen: Pen, - contractAddress: string, - msg: object, -): Promise { - const memo = "Time for action"; - const theMsg: MsgExecuteContract = { - type: "wasm/execute", - value: { - sender: alice.address0, - contract: contractAddress, - msg: msg, - sent_funds: [], - }, - }; - const fee: StdFee = { - amount: coins(5000000, "ucosm"), - gas: "89000000", - }; - - const { account_number, sequence } = (await client.authAccounts(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", () => { - it("can be constructed", () => { - const client = new RestClient(wasmd.endpoint); - expect(client).toBeTruthy(); - }); - - describe("txsQuery", () => { - it("can query by tags (module + code_id)", async () => { - pendingWithoutWasmd(); - const client = new RestClient(wasmd.endpoint); - const result = await client.txsQuery(`message.module=wasm&message.code_id=${deployedErc20.codeId}`); - expect(parseInt(result.count, 10)).toBeGreaterThanOrEqual(4); - - // Check first 4 results - const [store, hash, isa, jade] = result.txs.map((tx) => fromOneElementArray(tx.tx.value.msg)); - assert(isMsgStoreCode(store)); - assert(isMsgInstantiateContract(hash)); - assert(isMsgInstantiateContract(isa)); - assert(isMsgInstantiateContract(jade)); - expect(store.value).toEqual( - jasmine.objectContaining({ - sender: alice.address0, - source: deployedErc20.source, - builder: deployedErc20.builder, - }), - ); - expect(hash.value).toEqual({ - code_id: deployedErc20.codeId.toString(), - init_funds: [], - init_msg: jasmine.objectContaining({ - symbol: "HASH", - }), - label: "HASH", - sender: alice.address0, - }); - expect(isa.value).toEqual({ - code_id: deployedErc20.codeId.toString(), - init_funds: [], - init_msg: jasmine.objectContaining({ symbol: "ISA" }), - label: "ISA", - sender: alice.address0, - }); - expect(jade.value).toEqual({ - code_id: deployedErc20.codeId.toString(), - init_funds: [], - init_msg: jasmine.objectContaining({ symbol: "JADE" }), - label: "JADE", - sender: alice.address0, - admin: alice.address1, - }); - }); - - // 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 uploads = await client.txsQuery( - `message.module=wasm&message.code_id=${deployedErc20.codeId}&message.action=store-code`, - ); - expect(parseInt(uploads.count, 10)).toEqual(1); - const store = fromOneElementArray(uploads.txs[0].tx.value.msg); - assert(isMsgStoreCode(store)); - expect(store.value).toEqual( - jasmine.objectContaining({ - sender: alice.address0, - source: deployedErc20.source, - builder: deployedErc20.builder, - }), - ); - } - - { - const instantiations = await client.txsQuery( - `message.module=wasm&message.code_id=${deployedErc20.codeId}&message.action=instantiate`, - ); - expect(parseInt(instantiations.count, 10)).toBeGreaterThanOrEqual(3); - const [hash, isa, jade] = instantiations.txs.map((tx) => fromOneElementArray(tx.tx.value.msg)); - assert(isMsgInstantiateContract(hash)); - assert(isMsgInstantiateContract(isa)); - assert(isMsgInstantiateContract(jade)); - expect(hash.value).toEqual({ - code_id: deployedErc20.codeId.toString(), - init_funds: [], - init_msg: jasmine.objectContaining({ - symbol: "HASH", - }), - label: "HASH", - sender: alice.address0, - }); - expect(isa.value).toEqual({ - code_id: deployedErc20.codeId.toString(), - init_funds: [], - init_msg: jasmine.objectContaining({ symbol: "ISA" }), - label: "ISA", - sender: alice.address0, - }); - expect(jade.value).toEqual({ - code_id: deployedErc20.codeId.toString(), - init_funds: [], - init_msg: jasmine.objectContaining({ symbol: "JADE" }), - label: "JADE", - sender: alice.address0, - admin: alice.address1, - }); - } - }); - }); - - describe("postTx", () => { - it("can upload, instantiate and execute wasm", async () => { - pendingWithoutWasmd(); - const pen = await Secp256k1Pen.fromMnemonic(alice.mnemonic); - const client = new RestClient(wasmd.endpoint); - - const transferAmount = [coin(1234, "ucosm"), coin(321, "ustake")]; - const beneficiaryAddress = makeRandomAddress(); - - let codeId: number; - - // upload - { - // console.log("Raw log:", result.raw_log); - const result = await uploadContract(client, pen, getHackatom()); - expect(result.code).toBeFalsy(); - const logs = parseLogs(result.logs); - const codeIdAttr = findAttribute(logs, "message", "code_id"); - codeId = Number.parseInt(codeIdAttr.value, 10); - expect(codeId).toBeGreaterThanOrEqual(1); - expect(codeId).toBeLessThanOrEqual(200); - expect(result.data).toEqual(toHex(toAscii(`${codeId}`)).toUpperCase()); - } - - let contractAddress: string; - - // instantiate - { - const result = await instantiateContract(client, pen, codeId, beneficiaryAddress, transferAmount); - expect(result.code).toBeFalsy(); - // console.log("Raw log:", result.raw_log); - const logs = parseLogs(result.logs); - const contractAddressAttr = findAttribute(logs, "message", "contract_address"); - contractAddress = contractAddressAttr.value; - const amountAttr = findAttribute(logs, "transfer", "amount"); - 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; - expect(balance).toEqual(transferAmount); - } - - // execute - { - const result = await executeContract(client, pen, contractAddress, { release: {} }); - expect(result.data).toEqual("F00BAA"); - expect(result.code).toBeFalsy(); - // console.log("Raw log:", result.logs); - const logs = parseLogs(result.logs); - const wasmEvent = logs.find(() => true)?.events.find((e) => e.type === "wasm"); - assert(wasmEvent, "Event of type wasm expected"); - expect(wasmEvent.attributes).toContain({ key: "action", value: "release" }); - expect(wasmEvent.attributes).toContain({ - key: "destination", - value: beneficiaryAddress, - }); - - // Verify token transfer from contract to beneficiary - const beneficiaryBalance = (await client.authAccounts(beneficiaryAddress)).result.value.coins; - expect(beneficiaryBalance).toEqual(transferAmount); - const contractBalance = (await client.authAccounts(contractAddress)).result.value.coins; - expect(contractBalance).toEqual([]); - } - }); - }); - - // The /wasm endpoints - - describe("query", () => { - it("can list upload code", async () => { - pendingWithoutWasmd(); - const pen = await Secp256k1Pen.fromMnemonic(alice.mnemonic); - const client = new RestClient(wasmd.endpoint); - - // check with contracts were here first to compare - const existingInfos = await client.listCodeInfo(); - existingInfos.forEach((val, idx) => expect(val.id).toEqual(idx + 1)); - const numExisting = existingInfos.length; - - // upload data - const hackatom = getHackatom(); - const result = await uploadContract(client, pen, hackatom); - expect(result.code).toBeFalsy(); - const logs = parseLogs(result.logs); - const codeIdAttr = findAttribute(logs, "message", "code_id"); - const codeId = Number.parseInt(codeIdAttr.value, 10); - - // ensure we were added to the end of the list - const newInfos = await client.listCodeInfo(); - expect(newInfos.length).toEqual(numExisting + 1); - const lastInfo = newInfos[newInfos.length - 1]; - expect(lastInfo.id).toEqual(codeId); - expect(lastInfo.creator).toEqual(alice.address0); - - // ensure metadata is present - expect(lastInfo.source).toEqual(hackatom.source); - expect(lastInfo.builder).toEqual(hackatom.builder); - - // check code hash matches expectation - const wasmHash = new Sha256(hackatom.data).digest(); - expect(lastInfo.data_hash.toLowerCase()).toEqual(toHex(wasmHash)); - - // download code and check against auto-gen - const { data } = await client.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 beneficiaryAddress = makeRandomAddress(); - const transferAmount: readonly Coin[] = [ - { - amount: "707707", - denom: "ucosm", - }, - ]; - - // reuse an existing contract, or upload if needed - let codeId: number; - const existingInfos = await client.listCodeInfo(); - if (existingInfos.length > 0) { - codeId = existingInfos[existingInfos.length - 1].id; - } else { - const uploadResult = await uploadContract(client, pen, getHackatom()); - expect(uploadResult.code).toBeFalsy(); - const uploadLogs = parseLogs(uploadResult.logs); - const codeIdAttr = findAttribute(uploadLogs, "message", "code_id"); - codeId = Number.parseInt(codeIdAttr.value, 10); - } - - // create new instance and compare before and after - const existingContractsByCode = await client.listContractsByCodeId(codeId); - for (const contract of existingContractsByCode) { - expect(contract.address).toMatch(bech32AddressMatcher); - expect(contract.code_id).toEqual(codeId); - expect(contract.creator).toMatch(bech32AddressMatcher); - expect(contract.label).toMatch(/^.+$/); - } - - const result = await instantiateContract(client, pen, codeId, beneficiaryAddress, transferAmount); - expect(result.code).toBeFalsy(); - const logs = parseLogs(result.logs); - const contractAddressAttr = findAttribute(logs, "message", "contract_address"); - const myAddress = contractAddressAttr.value; - - const newContractsByCode = await client.listContractsByCodeId(codeId); - expect(newContractsByCode.length).toEqual(existingContractsByCode.length + 1); - const newContract = newContractsByCode[newContractsByCode.length - 1]; - expect(newContract).toEqual( - jasmine.objectContaining({ - code_id: codeId, - creator: alice.address0, - label: "my escrow", - }), - ); - - // check out info - const myInfo = await client.getContractInfo(myAddress); - assert(myInfo); - expect(myInfo).toEqual( - jasmine.objectContaining({ - code_id: codeId, - creator: alice.address0, - init_msg: jasmine.objectContaining({ - beneficiary: beneficiaryAddress, - }), - }), - ); - expect(myInfo.admin).toBeUndefined(); - - // make sure random addresses don't give useful info - const nonExistentAddress = makeRandomAddress(); - expect(await client.getContractInfo(nonExistentAddress)).toBeNull(); - }); - - describe("contract state", () => { - const client = new RestClient(wasmd.endpoint); - const noContract = makeRandomAddress(); - const expectedKey = toAscii("config"); - let contractAddress: string | undefined; - - beforeAll(async () => { - if (wasmdEnabled()) { - const pen = await Secp256k1Pen.fromMnemonic(alice.mnemonic); - const uploadResult = await uploadContract(client, pen, getHackatom()); - assert(!uploadResult.code); - const uploadLogs = parseLogs(uploadResult.logs); - const codeId = Number.parseInt(findAttribute(uploadLogs, "message", "code_id").value, 10); - const instantiateResult = await instantiateContract(client, pen, codeId, makeRandomAddress()); - assert(!instantiateResult.code); - const instantiateLogs = parseLogs(instantiateResult.logs); - const contractAddressAttr = findAttribute(instantiateLogs, "message", "contract_address"); - contractAddress = contractAddressAttr.value; - } - }); - - it("can get all state", async () => { - pendingWithoutWasmd(); - - // get contract state - const state = await client.getAllContractState(contractAddress!); - expect(state.length).toEqual(1); - const data = state[0]; - expect(data.key).toEqual(expectedKey); - const value = JSON.parse(fromAscii(data.val)); - expect(value.verifier).toBeDefined(); - expect(value.beneficiary).toBeDefined(); - - // bad address is empty array - const noContractState = await client.getAllContractState(noContract); - expect(noContractState).toEqual([]); - }); - - it("can query by key", async () => { - pendingWithoutWasmd(); - - // query by one key - const raw = await client.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")); - expect(missing).toBeNull(); - - // bad address is null - const noContractModel = await client.queryContractRaw(noContract, expectedKey); - expect(noContractModel).toBeNull(); - }); - - it("can make smart queries", async () => { - pendingWithoutWasmd(); - - // we can query the verifier properly - const resultDocument = await client.queryContractSmart(contractAddress!, { verifier: {} }); - expect(resultDocument).toEqual({ verifier: alice.address0 }); - - // invalid query syntax throws an error - await client.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( - () => fail("shouldn't succeed"), - (error) => expect(error).toMatch("not found"), - ); - }); - }); - }); -}); diff --git a/packages/cosmwasm/src/restclient.ts b/packages/cosmwasm/src/restclient.ts deleted file mode 100644 index d5406da6..00000000 --- a/packages/cosmwasm/src/restclient.ts +++ /dev/null @@ -1,163 +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; - /** - * 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; -} - -/** 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/types/index.d.ts b/packages/cosmwasm/types/index.d.ts index a56155bb..6a9a884b 100644 --- a/packages/cosmwasm/types/index.d.ts +++ b/packages/cosmwasm/types/index.d.ts @@ -1,7 +1,6 @@ import * as logs from "./logs"; export { logs }; export { setupWasmExtension, WasmExtension } from "./lcdapi/wasm"; -export { RestClient } from "./restclient"; export { Account, Block, diff --git a/packages/cosmwasm/types/restclient.d.ts b/packages/cosmwasm/types/restclient.d.ts deleted file mode 100644 index 43e4be1e..00000000 --- a/packages/cosmwasm/types/restclient.d.ts +++ /dev/null @@ -1,67 +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; - /** - * 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; -} -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/sdk38/src/index.ts b/packages/sdk38/src/index.ts index d6053e6e..7c7d6b96 100644 --- a/packages/sdk38/src/index.ts +++ b/packages/sdk38/src/index.ts @@ -37,7 +37,6 @@ export { SupplyExtension, TxsResponse, } from "./lcdapi"; -export { RestClient } from "./restclient"; 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/restclient.spec.ts b/packages/sdk38/src/restclient.spec.ts deleted file mode 100644 index 14d18f61..00000000 --- a/packages/sdk38/src/restclient.spec.ts +++ /dev/null @@ -1,885 +0,0 @@ -/* 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 { TxsResponse } from "./lcdapi"; -import { parseLogs } from "./logs"; -import { MsgSend } from "./msgs"; -import { makeCosmoshubPath, Secp256k1Pen } from "./pen"; -import { encodeBech32Pubkey } from "./pubkey"; -import { RestClient } from "./restclient"; -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 } from "./types"; - -describe("RestClient", () => { - const defaultRecipientAddress = makeRandomAddress(); - - it("can be constructed", () => { - const client = new RestClient(wasmd.endpoint); - expect(client).toBeTruthy(); - }); - - // The /auth endpoints - - 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, - }, - }); - }); - - // 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 = new RestClient(wasmd.endpoint); - const { result } = await client.authAccounts(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 = new RestClient(wasmd.endpoint); - const nonExistentAccount = makeRandomAddress(); - const { result } = await client.authAccounts(nonExistentAccount); - expect(result).toEqual({ - type: "cosmos-sdk/Account", - value: jasmine.objectContaining({ address: "" }), - }); - }); - }); - - // The /blocks endpoints - - describe("blocksLatest", () => { - it("works", async () => { - pendingWithoutWasmd(); - const client = new RestClient(wasmd.endpoint); - const response = await client.blocksLatest(); - - // id - expect(response.block_id.hash).toMatch(tendermintIdMatcher); - - // 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, - ); - 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$/), - }); - }); - }); - - // The /txs endpoints - - describe("txById", () => { - let successful: - | { - readonly sender: string; - readonly recipient: string; - readonly hash: string; - } - | undefined; - let unsuccessful: - | { - readonly sender: string; - readonly recipient: string; - readonly hash: string; - } - | undefined; - - beforeAll(async () => { - if (wasmdEnabled()) { - const pen = await Secp256k1Pen.fromMnemonic(faucet.mnemonic); - const client = new SigningCosmosClient(wasmd.endpoint, faucet.address, (signBytes) => - pen.sign(signBytes), - ); - - { - const recipient = makeRandomAddress(); - const transferAmount = { - denom: "ucosm", - amount: "1234567", - }; - const result = await client.sendTokens(recipient, [transferAmount]); - successful = { - sender: faucet.address, - recipient: recipient, - hash: result.transactionHash, - }; - } - - { - const memo = "Sending more than I can afford"; - const recipient = makeRandomAddress(); - const transferAmount = [ - { - denom: "ucosm", - amount: "123456700000000", - }, - ]; - const sendMsg: MsgSend = { - type: "cosmos-sdk/MsgSend", - value: { - // eslint-disable-next-line @typescript-eslint/camelcase - from_address: faucet.address, - // eslint-disable-next-line @typescript-eslint/camelcase - to_address: recipient, - amount: transferAmount, - }, - }; - const fee = { - amount: [ - { - denom: "ucosm", - amount: "2000", - }, - ], - gas: "80000", // 80k - }; - const { accountNumber, sequence } = await client.getNonce(); - const chainId = await client.getChainId(); - const signBytes = makeSignBytes([sendMsg], fee, chainId, memo, accountNumber, sequence); - const signature = await pen.sign(signBytes); - const signedTx = { - msg: [sendMsg], - fee: fee, - memo: memo, - signatures: [signature], - }; - const transactionId = await client.getIdentifier({ type: "cosmos-sdk/StdTx", value: signedTx }); - const result = await client.postTx(signedTx); - assert(isPostTxFailure(result)); - unsuccessful = { - sender: faucet.address, - recipient: recipient, - hash: transactionId, - }; - } - - await sleep(75); // wait until transactions are indexed - } - }); - - it("works for successful transaction", async () => { - pendingWithoutWasmd(); - assert(successful); - const client = new RestClient(wasmd.endpoint); - const result = await client.txById(successful.hash); - expect(result.height).toBeGreaterThanOrEqual(1); - expect(result.txhash).toEqual(successful.hash); - expect(result.codespace).toBeUndefined(); - expect(result.code).toBeUndefined(); - const logs = parseLogs(result.logs); - expect(logs).toEqual([ - { - msg_index: 0, - log: "", - events: [ - { - type: "message", - attributes: [ - { key: "action", value: "send" }, - { key: "sender", value: successful.sender }, - { key: "module", value: "bank" }, - ], - }, - { - type: "transfer", - attributes: [ - { key: "recipient", value: successful.recipient }, - { key: "sender", value: successful.sender }, - { key: "amount", value: "1234567ucosm" }, - ], - }, - ], - }, - ]); - }); - - it("works for unsuccessful transaction", async () => { - pendingWithoutWasmd(); - assert(unsuccessful); - const client = new RestClient(wasmd.endpoint); - const result = await client.txById(unsuccessful.hash); - expect(result.height).toBeGreaterThanOrEqual(1); - expect(result.txhash).toEqual(unsuccessful.hash); - expect(result.codespace).toEqual("sdk"); - expect(result.code).toEqual(5); - expect(result.logs).toBeUndefined(); - expect(result.raw_log).toContain("insufficient funds"); - }); - }); - - describe("txsQuery", () => { - let posted: - | { - readonly sender: string; - readonly recipient: string; - readonly hash: string; - readonly height: number; - readonly tx: TxsResponse; - } - | undefined; - - beforeAll(async () => { - if (wasmdEnabled()) { - const pen = await Secp256k1Pen.fromMnemonic(faucet.mnemonic); - const client = new SigningCosmosClient(wasmd.endpoint, faucet.address, (signBytes) => - pen.sign(signBytes), - ); - - const recipient = makeRandomAddress(); - const transferAmount = [ - { - denom: "ucosm", - amount: "1234567", - }, - ]; - 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); - posted = { - sender: faucet.address, - recipient: recipient, - hash: result.transactionHash, - height: Number.parseInt(txDetails.height, 10), - tx: txDetails, - }; - } - }); - - it("can query transactions by height", async () => { - pendingWithoutWasmd(); - assert(posted); - const client = new RestClient(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 - limit: "26", - page_number: "1", - page_total: "1", - total_count: jasmine.stringMatching(/^(1|2|3|4|5)$/), // 1-5 transactions as string - txs: jasmine.arrayContaining([posted.tx]), - }); - }); - - it("can query transactions by ID", async () => { - pendingWithoutWasmd(); - assert(posted); - const client = new RestClient(wasmd.endpoint); - const result = await client.txsQuery(`tx.hash=${posted.hash}&limit=26`); - expect(result).toEqual({ - count: "1", - limit: "26", - page_number: "1", - page_total: "1", - total_count: "1", - txs: [posted.tx], - }); - }); - - it("can query transactions by sender", async () => { - pendingWithoutWasmd(); - assert(posted); - const client = new RestClient(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); - expect(parseInt(result.page_number, 10)).toEqual(1); - expect(parseInt(result.page_total, 10)).toEqual(1); - expect(parseInt(result.total_count, 10)).toBeGreaterThanOrEqual(1); - expect(result.txs.length).toBeGreaterThanOrEqual(1); - expect(result.txs[result.txs.length - 1]).toEqual(posted.tx); - }); - - it("can query transactions by recipient", async () => { - pendingWithoutWasmd(); - assert(posted); - const client = new RestClient(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); - expect(parseInt(result.page_number, 10)).toEqual(1); - expect(parseInt(result.page_total, 10)).toEqual(1); - expect(parseInt(result.total_count, 10)).toEqual(1); - expect(result.txs.length).toBeGreaterThanOrEqual(1); - expect(result.txs[result.txs.length - 1]).toEqual(posted.tx); - }); - - it("can filter by tx.hash and tx.minheight", async () => { - pending("This combination is broken 🤷‍♂️. Handle client-side at higher level."); - pendingWithoutWasmd(); - assert(posted); - const client = new RestClient(wasmd.endpoint); - const hashQuery = `tx.hash=${posted.hash}`; - - { - const { count } = await client.txsQuery(`${hashQuery}&tx.minheight=0`); - expect(count).toEqual("1"); - } - - { - const { count } = await client.txsQuery(`${hashQuery}&tx.minheight=${posted.height - 1}`); - expect(count).toEqual("1"); - } - - { - const { count } = await client.txsQuery(`${hashQuery}&tx.minheight=${posted.height}`); - expect(count).toEqual("1"); - } - - { - const { count } = await client.txsQuery(`${hashQuery}&tx.minheight=${posted.height + 1}`); - expect(count).toEqual("0"); - } - }); - - it("can filter by recipient and tx.minheight", async () => { - pendingWithoutWasmd(); - assert(posted); - const client = new RestClient(wasmd.endpoint); - const recipientQuery = `transfer.recipient=${posted.recipient}`; - - { - const { count } = await client.txsQuery(`${recipientQuery}&tx.minheight=0`); - expect(count).toEqual("1"); - } - - { - const { count } = await client.txsQuery(`${recipientQuery}&tx.minheight=${posted.height - 1}`); - expect(count).toEqual("1"); - } - - { - const { count } = await client.txsQuery(`${recipientQuery}&tx.minheight=${posted.height}`); - expect(count).toEqual("1"); - } - - { - const { count } = await client.txsQuery(`${recipientQuery}&tx.minheight=${posted.height + 1}`); - expect(count).toEqual("0"); - } - }); - - it("can filter by recipient and tx.maxheight", async () => { - pendingWithoutWasmd(); - assert(posted); - const client = new RestClient(wasmd.endpoint); - const recipientQuery = `transfer.recipient=${posted.recipient}`; - - { - const { count } = await client.txsQuery(`${recipientQuery}&tx.maxheight=9999999999999`); - expect(count).toEqual("1"); - } - - { - const { count } = await client.txsQuery(`${recipientQuery}&tx.maxheight=${posted.height + 1}`); - expect(count).toEqual("1"); - } - - { - const { count } = await client.txsQuery(`${recipientQuery}&tx.maxheight=${posted.height}`); - expect(count).toEqual("1"); - } - - { - const { count } = await client.txsQuery(`${recipientQuery}&tx.maxheight=${posted.height - 1}`); - expect(count).toEqual("0"); - } - }); - }); - - describe("encodeTx", () => { - it("works for cosmoshub example", async () => { - pendingWithoutWasmd(); - const client = new RestClient(wasmd.endpoint); - const response = await client.encodeTx(cosmoshub.tx); - expect(response).toEqual( - jasmine.objectContaining({ - tx: cosmoshub.tx_data, - }), - ); - }); - }); - - describe("postTx", () => { - it("can send tokens", async () => { - pendingWithoutWasmd(); - const pen = await Secp256k1Pen.fromMnemonic(faucet.mnemonic); - - const memo = "My first contract on chain"; - const theMsg: MsgSend = { - type: "cosmos-sdk/MsgSend", - value: { - from_address: faucet.address, - to_address: defaultRecipientAddress, - amount: [ - { - denom: "ucosm", - amount: "1234567", - }, - ], - }, - }; - - const fee: StdFee = { - amount: [ - { - amount: "5000", - denom: "ucosm", - }, - ], - gas: "890000", - }; - - const client = new RestClient(wasmd.endpoint); - const { account_number, sequence } = (await client.authAccounts(faucet.address)).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); - const result = await client.postTx(signedTx); - expect(result.code).toBeUndefined(); - expect(result).toEqual({ - height: jasmine.stringMatching(nonNegativeIntegerMatcher), - txhash: jasmine.stringMatching(tendermintIdMatcher), - // code is not set - raw_log: jasmine.stringMatching(/^\[.+\]$/i), - logs: jasmine.any(Array), - gas_wanted: jasmine.stringMatching(nonNegativeIntegerMatcher), - gas_used: jasmine.stringMatching(nonNegativeIntegerMatcher), - }); - }); - - it("can't send transaction with additional signatures", async () => { - pendingWithoutWasmd(); - const account1 = await Secp256k1Pen.fromMnemonic(faucet.mnemonic, makeCosmoshubPath(0)); - const account2 = await Secp256k1Pen.fromMnemonic(faucet.mnemonic, makeCosmoshubPath(1)); - const account3 = await Secp256k1Pen.fromMnemonic(faucet.mnemonic, makeCosmoshubPath(2)); - const address1 = rawSecp256k1PubkeyToAddress(account1.pubkey, "cosmos"); - const address2 = rawSecp256k1PubkeyToAddress(account2.pubkey, "cosmos"); - const address3 = rawSecp256k1PubkeyToAddress(account3.pubkey, "cosmos"); - - const memo = "My first contract on chain"; - const theMsg: MsgSend = { - type: "cosmos-sdk/MsgSend", - value: { - from_address: address1, - to_address: defaultRecipientAddress, - amount: [ - { - denom: "ucosm", - amount: "1234567", - }, - ], - }, - }; - - const fee: StdFee = { - amount: [ - { - amount: "5000", - denom: "ucosm", - }, - ], - 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 signBytes1 = makeSignBytes([theMsg], fee, wasmd.chainId, memo, an1, sequence1); - const signBytes2 = makeSignBytes([theMsg], fee, wasmd.chainId, memo, an2, sequence2); - const signBytes3 = makeSignBytes([theMsg], fee, wasmd.chainId, memo, an3, sequence3); - const signature1 = await account1.sign(signBytes1); - const signature2 = await account2.sign(signBytes2); - const signature3 = await account3.sign(signBytes3); - const signedTx = { - msg: [theMsg], - fee: fee, - memo: memo, - signatures: [signature1, signature2, signature3], - }; - const postResult = await client.postTx(signedTx); - expect(postResult.code).toEqual(4); - expect(postResult.raw_log).toContain("wrong number of signers"); - }); - - it("can send multiple messages with one signature", async () => { - pendingWithoutWasmd(); - const account1 = await Secp256k1Pen.fromMnemonic(faucet.mnemonic, makeCosmoshubPath(0)); - const address1 = rawSecp256k1PubkeyToAddress(account1.pubkey, "cosmos"); - - const memo = "My first contract on chain"; - const msg1: MsgSend = { - type: "cosmos-sdk/MsgSend", - value: { - from_address: address1, - to_address: defaultRecipientAddress, - amount: [ - { - denom: "ucosm", - amount: "1234567", - }, - ], - }, - }; - const msg2: MsgSend = { - type: "cosmos-sdk/MsgSend", - value: { - from_address: address1, - to_address: defaultRecipientAddress, - amount: [ - { - denom: "ucosm", - amount: "7654321", - }, - ], - }, - }; - - const fee: StdFee = { - amount: [ - { - amount: "5000", - denom: "ucosm", - }, - ], - gas: "890000", - }; - - const client = new RestClient(wasmd.endpoint); - const { account_number, sequence } = (await client.authAccounts(address1)).result.value; - - const signBytes = makeSignBytes([msg1, msg2], fee, wasmd.chainId, memo, account_number, sequence); - const signature1 = await account1.sign(signBytes); - const signedTx = { - msg: [msg1, msg2], - fee: fee, - memo: memo, - signatures: [signature1], - }; - const postResult = await client.postTx(signedTx); - expect(postResult.code).toBeUndefined(); - }); - - it("can send multiple messages with multiple signatures", async () => { - pendingWithoutWasmd(); - const account1 = await Secp256k1Pen.fromMnemonic(faucet.mnemonic, makeCosmoshubPath(0)); - const account2 = await Secp256k1Pen.fromMnemonic(faucet.mnemonic, makeCosmoshubPath(1)); - const address1 = rawSecp256k1PubkeyToAddress(account1.pubkey, "cosmos"); - const address2 = rawSecp256k1PubkeyToAddress(account2.pubkey, "cosmos"); - - const memo = "My first contract on chain"; - const msg1: MsgSend = { - type: "cosmos-sdk/MsgSend", - value: { - from_address: address1, - to_address: defaultRecipientAddress, - amount: [ - { - denom: "ucosm", - amount: "1234567", - }, - ], - }, - }; - const msg2: MsgSend = { - type: "cosmos-sdk/MsgSend", - value: { - from_address: address2, - to_address: defaultRecipientAddress, - amount: [ - { - denom: "ucosm", - amount: "7654321", - }, - ], - }, - }; - - const fee: StdFee = { - amount: [ - { - amount: "5000", - denom: "ucosm", - }, - ], - 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 signBytes1 = makeSignBytes([msg2, msg1], fee, wasmd.chainId, memo, an1, sequence1); - const signBytes2 = makeSignBytes([msg2, msg1], fee, wasmd.chainId, memo, an2, sequence2); - const signature1 = await account1.sign(signBytes1); - const signature2 = await account2.sign(signBytes2); - const signedTx = { - msg: [msg2, msg1], - fee: fee, - memo: memo, - signatures: [signature2, signature1], - }; - const postResult = await client.postTx(signedTx); - expect(postResult.code).toBeUndefined(); - - await sleep(500); - const searched = await client.txsQuery(`tx.hash=${postResult.txhash}`); - expect(searched.txs.length).toEqual(1); - expect(searched.txs[0].tx.value.signatures).toEqual([signature2, signature1]); - }); - - it("can't send transaction with wrong signature order (1)", async () => { - pendingWithoutWasmd(); - const account1 = await Secp256k1Pen.fromMnemonic(faucet.mnemonic, makeCosmoshubPath(0)); - const account2 = await Secp256k1Pen.fromMnemonic(faucet.mnemonic, makeCosmoshubPath(1)); - const address1 = rawSecp256k1PubkeyToAddress(account1.pubkey, "cosmos"); - const address2 = rawSecp256k1PubkeyToAddress(account2.pubkey, "cosmos"); - - const memo = "My first contract on chain"; - const msg1: MsgSend = { - type: "cosmos-sdk/MsgSend", - value: { - from_address: address1, - to_address: defaultRecipientAddress, - amount: [ - { - denom: "ucosm", - amount: "1234567", - }, - ], - }, - }; - const msg2: MsgSend = { - type: "cosmos-sdk/MsgSend", - value: { - from_address: address2, - to_address: defaultRecipientAddress, - amount: [ - { - denom: "ucosm", - amount: "7654321", - }, - ], - }, - }; - - const fee: StdFee = { - amount: [ - { - amount: "5000", - denom: "ucosm", - }, - ], - 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 signBytes1 = makeSignBytes([msg1, msg2], fee, wasmd.chainId, memo, an1, sequence1); - const signBytes2 = makeSignBytes([msg1, msg2], fee, wasmd.chainId, memo, an2, sequence2); - const signature1 = await account1.sign(signBytes1); - const signature2 = await account2.sign(signBytes2); - const signedTx = { - msg: [msg1, msg2], - fee: fee, - memo: memo, - signatures: [signature2, signature1], - }; - const postResult = await client.postTx(signedTx); - expect(postResult.code).toEqual(8); - }); - - it("can't send transaction with wrong signature order (2)", async () => { - pendingWithoutWasmd(); - const account1 = await Secp256k1Pen.fromMnemonic(faucet.mnemonic, makeCosmoshubPath(0)); - const account2 = await Secp256k1Pen.fromMnemonic(faucet.mnemonic, makeCosmoshubPath(1)); - const address1 = rawSecp256k1PubkeyToAddress(account1.pubkey, "cosmos"); - const address2 = rawSecp256k1PubkeyToAddress(account2.pubkey, "cosmos"); - - const memo = "My first contract on chain"; - const msg1: MsgSend = { - type: "cosmos-sdk/MsgSend", - value: { - from_address: address1, - to_address: defaultRecipientAddress, - amount: [ - { - denom: "ucosm", - amount: "1234567", - }, - ], - }, - }; - const msg2: MsgSend = { - type: "cosmos-sdk/MsgSend", - value: { - from_address: address2, - to_address: defaultRecipientAddress, - amount: [ - { - denom: "ucosm", - amount: "7654321", - }, - ], - }, - }; - - const fee: StdFee = { - amount: [ - { - amount: "5000", - denom: "ucosm", - }, - ], - 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 signBytes1 = makeSignBytes([msg2, msg1], fee, wasmd.chainId, memo, an1, sequence1); - const signBytes2 = makeSignBytes([msg2, msg1], fee, wasmd.chainId, memo, an2, sequence2); - const signature1 = await account1.sign(signBytes1); - const signature2 = await account2.sign(signBytes2); - const signedTx = { - msg: [msg2, msg1], - fee: fee, - memo: memo, - signatures: [signature1, signature2], - }; - const postResult = await client.postTx(signedTx); - expect(postResult.code).toEqual(8); - }); - }); -}); diff --git a/packages/sdk38/src/restclient.ts b/packages/sdk38/src/restclient.ts deleted file mode 100644 index 81d1d85f..00000000 --- a/packages/sdk38/src/restclient.ts +++ /dev/null @@ -1,168 +0,0 @@ -import { isNonNullObject } from "@cosmjs/utils"; -import axios, { AxiosError, AxiosInstance } from "axios"; - -import { - AuthAccountsResponse, - BlockResponse, - BroadcastMode, - EncodeTxResponse, - NodeInfoResponse, - PostTxsResponse, - SearchTxsResponse, - TxsResponse, -} from "./lcdapi"; -import { CosmosSdkTx, StdTx } from "./types"; - -// 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; - } -} - -/** - * @deprecated use LcdClient. - */ -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/types/index.d.ts b/packages/sdk38/types/index.d.ts index 38123a21..ea017984 100644 --- a/packages/sdk38/types/index.d.ts +++ b/packages/sdk38/types/index.d.ts @@ -35,7 +35,6 @@ export { SupplyExtension, TxsResponse, } from "./lcdapi"; -export { RestClient } from "./restclient"; 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/restclient.d.ts b/packages/sdk38/types/restclient.d.ts deleted file mode 100644 index 641b04c7..00000000 --- a/packages/sdk38/types/restclient.d.ts +++ /dev/null @@ -1,48 +0,0 @@ -import { - AuthAccountsResponse, - BlockResponse, - BroadcastMode, - EncodeTxResponse, - NodeInfoResponse, - PostTxsResponse, - SearchTxsResponse, - TxsResponse, -} from "./lcdapi"; -import { CosmosSdkTx, StdTx } from "./types"; -/** - * @deprecated use LcdClient. - */ -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; -}