diff --git a/packages/bcp/package.json b/packages/bcp/package.json index f62bcf4c..5fc202bf 100644 --- a/packages/bcp/package.json +++ b/packages/bcp/package.json @@ -36,7 +36,8 @@ "pack-web": "yarn build-or-skip && webpack --mode development --config webpack.web.config.js" }, "dependencies": { - "@cosmwasm/sdk": "^0.8.0", + "@cosmwasm/cosmwasm": "^0.8.0", + "@cosmwasm/sdk38": "^0.8.0", "@iov/bcp": "^2.1.0", "@iov/crypto": "^2.1.0", "@iov/encoding": "^2.1.0", diff --git a/packages/bcp/src/address.ts b/packages/bcp/src/address.ts index e26cf556..4396357a 100644 --- a/packages/bcp/src/address.ts +++ b/packages/bcp/src/address.ts @@ -1,4 +1,4 @@ -import { pubkeyToAddress as sdkPubkeyToAddress, types } from "@cosmwasm/sdk"; +import { PubKey, pubkeyToAddress as sdkPubkeyToAddress, pubkeyType } from "@cosmwasm/sdk38"; import { Address, Algorithm, PubkeyBundle } from "@iov/bcp"; import { Secp256k1 } from "@iov/crypto"; import { Encoding } from "@iov/encoding"; @@ -7,15 +7,15 @@ const { toBase64 } = Encoding; // See https://github.com/tendermint/tendermint/blob/f2ada0a604b4c0763bda2f64fac53d506d3beca7/docs/spec/blockchain/encoding.md#public-key-cryptography export function pubkeyToAddress(pubkey: PubkeyBundle, prefix: string): Address { - let sdkKey: types.PubKey; + let sdkKey: PubKey; if (pubkey.algo === Algorithm.Secp256k1) { sdkKey = { - type: types.pubkeyType.secp256k1, + type: pubkeyType.secp256k1, value: toBase64(pubkey.data.length > 33 ? Secp256k1.compressPubkey(pubkey.data) : pubkey.data), }; } else if (pubkey.algo === Algorithm.Ed25519) { sdkKey = { - type: types.pubkeyType.ed25519, + type: pubkeyType.ed25519, value: toBase64(pubkey.data), }; } else { diff --git a/packages/bcp/src/cosmwasmcodec.ts b/packages/bcp/src/cosmwasmcodec.ts index d7c00754..ddaa88d9 100644 --- a/packages/bcp/src/cosmwasmcodec.ts +++ b/packages/bcp/src/cosmwasmcodec.ts @@ -1,5 +1,5 @@ /* eslint-disable @typescript-eslint/camelcase */ -import { makeSignBytes, marshalTx, unmarshalTx } from "@cosmwasm/sdk"; +import { makeSignBytes, marshalTx, unmarshalTx } from "@cosmwasm/sdk38"; import { Address, ChainId, diff --git a/packages/bcp/src/cosmwasmconnection.spec.ts b/packages/bcp/src/cosmwasmconnection.spec.ts index 4f4b716f..a661a87c 100644 --- a/packages/bcp/src/cosmwasmconnection.spec.ts +++ b/packages/bcp/src/cosmwasmconnection.spec.ts @@ -1,4 +1,4 @@ -import { decodeSignature } from "@cosmwasm/sdk"; +import { decodeSignature } from "@cosmwasm/sdk38"; import { Account, Address, diff --git a/packages/bcp/src/cosmwasmconnection.ts b/packages/bcp/src/cosmwasmconnection.ts index f458413c..b28c2c0f 100644 --- a/packages/bcp/src/cosmwasmconnection.ts +++ b/packages/bcp/src/cosmwasmconnection.ts @@ -1,4 +1,10 @@ -import { CosmWasmClient, findSequenceForSignedTx, IndexedTx, SearchTxFilter, types } from "@cosmwasm/sdk"; +import { + CosmWasmClient, + isMsgExecuteContract, + isMsgInstantiateContract, + isMsgStoreCode, +} from "@cosmwasm/cosmwasm"; +import { findSequenceForSignedTx, IndexedTx, isMsgSend, isStdTx, SearchTxFilter } from "@cosmwasm/sdk38"; import { Account, AccountQuery, @@ -272,7 +278,7 @@ export class CosmWasmConnection implements BlockchainConnection { public async postTx(tx: PostableBytes): Promise { const txAsJson = JSON.parse(Encoding.fromUtf8(tx)); - if (!types.isStdTx(txAsJson)) throw new Error("Postable bytes must contain a JSON encoded StdTx"); + if (!isStdTx(txAsJson)) throw new Error("Postable bytes must contain a JSON encoded StdTx"); const { transactionHash, rawLog } = await this.cosmWasmClient.postTx(txAsJson); const transactionId = transactionHash as TransactionId; const firstEvent: BlockInfo = { state: TransactionState.Pending }; @@ -473,12 +479,12 @@ export class CosmWasmConnection implements BlockchainConnection { if (!firstMsg) throw new Error("Got transaction without a first message. What is going on here?"); let senderAddress: string; - if (types.isMsgSend(firstMsg)) { + if (isMsgSend(firstMsg)) { senderAddress = firstMsg.value.from_address; } else if ( - types.isMsgStoreCode(firstMsg) || - types.isMsgInstantiateContract(firstMsg) || - types.isMsgExecuteContract(firstMsg) + isMsgStoreCode(firstMsg) || + isMsgInstantiateContract(firstMsg) || + isMsgExecuteContract(firstMsg) ) { senderAddress = firstMsg.value.sender; } else { diff --git a/packages/bcp/src/decode.spec.ts b/packages/bcp/src/decode.spec.ts index 5a641c4f..c97e20ed 100644 --- a/packages/bcp/src/decode.spec.ts +++ b/packages/bcp/src/decode.spec.ts @@ -1,5 +1,6 @@ /* eslint-disable @typescript-eslint/camelcase */ -import { Coin, IndexedTx, types } from "@cosmwasm/sdk"; +import { MsgExecuteContract } from "@cosmwasm/cosmwasm"; +import { Coin, IndexedTx, Msg, PubKey, StdSignature, StdTx } from "@cosmwasm/sdk38"; import { Address, Algorithm, isSendTransaction, SendTransaction, TokenTicker } from "@iov/bcp"; import { Encoding } from "@iov/encoding"; import { assert } from "@iov/utils"; @@ -84,7 +85,7 @@ describe("decode", () => { describe("decodePubkey", () => { it("works for secp256k1", () => { - const pubkey: types.PubKey = { + const pubkey: PubKey = { type: "tendermint/PubKeySecp256k1", value: "AtQaCqFnshaZQp6rIkvAPyzThvCvXSDO+9AzbxVErqJP", }; @@ -92,7 +93,7 @@ describe("decode", () => { }); it("works for ed25519", () => { - const pubkey: types.PubKey = { + const pubkey: PubKey = { type: "tendermint/PubKeyEd25519", value: "s69CnMgLTpuRyEfecjws3mWssBrOICUx8C2O1DkKSto=", }; @@ -104,7 +105,7 @@ describe("decode", () => { it("throws for unsupported types", () => { // https://github.com/tendermint/tendermint/blob/v0.33.0/crypto/sr25519/codec.go#L12 - const pubkey: types.PubKey = { + const pubkey: PubKey = { type: "tendermint/PubKeySr25519", value: "N4FJNPE5r/Twz55kO1QEIxyaGF5/HTXH6WgLQJWsy1o=", }; @@ -122,7 +123,7 @@ describe("decode", () => { describe("decodeFullSignature", () => { it("works", () => { - const fullSignature: types.StdSignature = { + const fullSignature: StdSignature = { pub_key: { type: "tendermint/PubKeySecp256k1", value: "AtQaCqFnshaZQp6rIkvAPyzThvCvXSDO+9AzbxVErqJP", @@ -145,7 +146,7 @@ describe("decode", () => { describe("parseMsg", () => { it("works for bank send transaction", () => { - const msg: types.Msg = { + const msg: Msg = { type: "cosmos-sdk/MsgSend", value: { from_address: "cosmos1h806c7khnvmjlywdrkdgk2vrayy2mmvf9rxk2r", @@ -164,7 +165,7 @@ describe("decode", () => { }); it("works for ERC20 send transaction", () => { - const msg: types.MsgExecuteContract = { + const msg: MsgExecuteContract = { type: "wasm/execute", value: { sender: "cosmos1h806c7khnvmjlywdrkdgk2vrayy2mmvf9rxk2r", @@ -218,7 +219,7 @@ describe("decode", () => { }); it("works for ERC20 send transaction", () => { - const msg: types.MsgExecuteContract = { + const msg: MsgExecuteContract = { type: "wasm/execute", value: { sender: "cosmos1h806c7khnvmjlywdrkdgk2vrayy2mmvf9rxk2r", @@ -232,7 +233,7 @@ describe("decode", () => { sent_funds: [], }, }; - const tx: types.StdTx = { + const tx: StdTx = { msg: [msg], memo: defaultMemo, fee: { diff --git a/packages/bcp/src/decode.ts b/packages/bcp/src/decode.ts index d258a5d2..cab2543b 100644 --- a/packages/bcp/src/decode.ts +++ b/packages/bcp/src/decode.ts @@ -1,4 +1,16 @@ -import { Coin, IndexedTx, types } from "@cosmwasm/sdk"; +import { isMsgExecuteContract } from "@cosmwasm/cosmwasm"; +import { + Coin, + IndexedTx, + isMsgSend, + isStdTx, + Msg, + PubKey, + pubkeyType, + StdFee, + StdSignature, + StdTx, +} from "@cosmwasm/sdk38"; import { Address, Algorithm, @@ -25,14 +37,14 @@ import { BankToken, Erc20Token } from "./types"; const { fromBase64 } = Encoding; -export function decodePubkey(pubkey: types.PubKey): PubkeyBundle { +export function decodePubkey(pubkey: PubKey): PubkeyBundle { switch (pubkey.type) { - case types.pubkeyType.secp256k1: + case pubkeyType.secp256k1: return { algo: Algorithm.Secp256k1, data: fromBase64(pubkey.value) as PubkeyBytes, }; - case types.pubkeyType.ed25519: + case pubkeyType.ed25519: return { algo: Algorithm.Ed25519, data: fromBase64(pubkey.value) as PubkeyBytes, @@ -46,7 +58,7 @@ export function decodeSignature(signature: string): SignatureBytes { return fromBase64(signature) as SignatureBytes; } -export function decodeFullSignature(signature: types.StdSignature, nonce: number): FullSignature { +export function decodeFullSignature(signature: StdSignature, nonce: number): FullSignature { return { nonce: nonce as Nonce, pubkey: decodePubkey(signature.pub_key), @@ -73,13 +85,13 @@ export function decodeAmount(tokens: readonly BankToken[], coin: Coin): Amount { } export function parseMsg( - msg: types.Msg, + msg: Msg, memo: string | undefined, chainId: ChainId, tokens: readonly BankToken[], erc20Tokens: readonly Erc20Token[], ): UnsignedTransaction { - if (types.isMsgSend(msg)) { + if (isMsgSend(msg)) { if (msg.value.amount.length !== 1) { throw new Error("Only MsgSend with one amount is supported"); } @@ -92,7 +104,7 @@ export function parseMsg( memo: memo, }; return send; - } else if (types.isMsgExecuteContract(msg)) { + } else if (isMsgExecuteContract(msg)) { const matchingTokenContract = erc20Tokens.find((t) => t.contractAddress === msg.value.contract); if (!matchingTokenContract) { return { @@ -130,7 +142,7 @@ export function parseMsg( } } -export function parseFee(fee: types.StdFee, tokens: readonly BankToken[]): Fee { +export function parseFee(fee: StdFee, tokens: readonly BankToken[]): Fee { if (fee.amount.length !== 1) { throw new Error("Only fee with one amount is supported"); } @@ -141,12 +153,12 @@ export function parseFee(fee: types.StdFee, tokens: readonly BankToken[]): Fee { } export function parseUnsignedTx( - txValue: types.StdTx, + txValue: StdTx, chainId: ChainId, tokens: readonly BankToken[], erc20Tokens: readonly Erc20Token[], ): UnsignedTransaction { - if (!types.isStdTx(txValue)) { + if (!isStdTx(txValue)) { throw new Error("Only StdTx is supported"); } if (txValue.msg.length !== 1) { @@ -164,7 +176,7 @@ export function parseUnsignedTx( } export function parseSignedTx( - txValue: types.StdTx, + txValue: StdTx, chainId: ChainId, nonce: Nonce, tokens: readonly BankToken[], diff --git a/packages/bcp/src/encode.ts b/packages/bcp/src/encode.ts index 9d20f3d8..f8820a2a 100644 --- a/packages/bcp/src/encode.ts +++ b/packages/bcp/src/encode.ts @@ -1,5 +1,14 @@ /* eslint-disable @typescript-eslint/camelcase */ -import { Coin, encodeSecp256k1Pubkey, encodeSecp256k1Signature, types } from "@cosmwasm/sdk"; +import { + Coin, + CosmosSdkTx, + encodeSecp256k1Pubkey, + encodeSecp256k1Signature, + PubKey, + pubkeyType, + StdFee, + StdSignature, +} from "@cosmwasm/sdk38"; import { Algorithm, Amount, @@ -18,13 +27,13 @@ import { BankToken, Erc20Token } from "./types"; const { toBase64 } = Encoding; // TODO: This function seems to be unused and is not well tested (e.g. uncompressed secp256k1 or ed25519) -export function encodePubkey(pubkey: PubkeyBundle): types.PubKey { +export function encodePubkey(pubkey: PubkeyBundle): PubKey { switch (pubkey.algo) { case Algorithm.Secp256k1: return encodeSecp256k1Pubkey(pubkey.data); case Algorithm.Ed25519: return { - type: types.pubkeyType.ed25519, + type: pubkeyType.ed25519, value: toBase64(pubkey.data), }; default: @@ -54,7 +63,7 @@ export function toBankCoin(amount: Amount, tokens: readonly BankToken[]): Coin { }; } -export function encodeFee(fee: Fee, tokens: readonly BankToken[]): types.StdFee { +export function encodeFee(fee: Fee, tokens: readonly BankToken[]): StdFee { if (fee.tokens === undefined) { throw new Error("Cannot encode fee without tokens"); } @@ -67,7 +76,7 @@ export function encodeFee(fee: Fee, tokens: readonly BankToken[]): types.StdFee }; } -export function encodeFullSignature(fullSignature: FullSignature): types.StdSignature { +export function encodeFullSignature(fullSignature: FullSignature): StdSignature { switch (fullSignature.pubkey.algo) { case Algorithm.Secp256k1: { const compressedPubkey = Secp256k1.compressPubkey(fullSignature.pubkey.data); @@ -83,7 +92,7 @@ export function buildUnsignedTx( tx: UnsignedTransaction, bankTokens: readonly BankToken[], erc20Tokens: readonly Erc20Token[] = [], -): types.CosmosSdkTx { +): CosmosSdkTx { if (!isSendTransaction(tx)) { throw new Error("Received transaction of unsupported kind"); } @@ -146,7 +155,7 @@ export function buildSignedTx( tx: SignedTransaction, bankTokens: readonly BankToken[], erc20Tokens: readonly Erc20Token[] = [], -): types.CosmosSdkTx { +): CosmosSdkTx { const built = buildUnsignedTx(tx.transaction, bankTokens, erc20Tokens); return { ...built, diff --git a/packages/bcp/types/decode.d.ts b/packages/bcp/types/decode.d.ts index ec69e99e..495a92f9 100644 --- a/packages/bcp/types/decode.d.ts +++ b/packages/bcp/types/decode.d.ts @@ -1,4 +1,4 @@ -import { Coin, IndexedTx, types } from "@cosmwasm/sdk"; +import { Coin, IndexedTx, Msg, PubKey, StdFee, StdSignature, StdTx } from "@cosmwasm/sdk38"; import { Amount, ChainId, @@ -14,27 +14,27 @@ import { } from "@iov/bcp"; import { Decimal } from "@iov/encoding"; import { BankToken, Erc20Token } from "./types"; -export declare function decodePubkey(pubkey: types.PubKey): PubkeyBundle; +export declare function decodePubkey(pubkey: PubKey): PubkeyBundle; export declare function decodeSignature(signature: string): SignatureBytes; -export declare function decodeFullSignature(signature: types.StdSignature, nonce: number): FullSignature; +export declare function decodeFullSignature(signature: StdSignature, nonce: number): FullSignature; export declare function coinToDecimal(tokens: readonly BankToken[], coin: Coin): readonly [Decimal, string]; export declare function decodeAmount(tokens: readonly BankToken[], coin: Coin): Amount; export declare function parseMsg( - msg: types.Msg, + msg: Msg, memo: string | undefined, chainId: ChainId, tokens: readonly BankToken[], erc20Tokens: readonly Erc20Token[], ): UnsignedTransaction; -export declare function parseFee(fee: types.StdFee, tokens: readonly BankToken[]): Fee; +export declare function parseFee(fee: StdFee, tokens: readonly BankToken[]): Fee; export declare function parseUnsignedTx( - txValue: types.StdTx, + txValue: StdTx, chainId: ChainId, tokens: readonly BankToken[], erc20Tokens: readonly Erc20Token[], ): UnsignedTransaction; export declare function parseSignedTx( - txValue: types.StdTx, + txValue: StdTx, chainId: ChainId, nonce: Nonce, tokens: readonly BankToken[], diff --git a/packages/bcp/types/encode.d.ts b/packages/bcp/types/encode.d.ts index f34e16f6..af4033d4 100644 --- a/packages/bcp/types/encode.d.ts +++ b/packages/bcp/types/encode.d.ts @@ -1,18 +1,18 @@ -import { Coin, types } from "@cosmwasm/sdk"; +import { Coin, CosmosSdkTx, PubKey, StdFee, StdSignature } from "@cosmwasm/sdk38"; import { Amount, Fee, FullSignature, PubkeyBundle, SignedTransaction, UnsignedTransaction } from "@iov/bcp"; import { BankToken, Erc20Token } from "./types"; -export declare function encodePubkey(pubkey: PubkeyBundle): types.PubKey; +export declare function encodePubkey(pubkey: PubkeyBundle): PubKey; export declare function toErc20Amount(amount: Amount, erc20Token: Erc20Token): string; export declare function toBankCoin(amount: Amount, tokens: readonly BankToken[]): Coin; -export declare function encodeFee(fee: Fee, tokens: readonly BankToken[]): types.StdFee; -export declare function encodeFullSignature(fullSignature: FullSignature): types.StdSignature; +export declare function encodeFee(fee: Fee, tokens: readonly BankToken[]): StdFee; +export declare function encodeFullSignature(fullSignature: FullSignature): StdSignature; export declare function buildUnsignedTx( tx: UnsignedTransaction, bankTokens: readonly BankToken[], erc20Tokens?: readonly Erc20Token[], -): types.CosmosSdkTx; +): CosmosSdkTx; export declare function buildSignedTx( tx: SignedTransaction, bankTokens: readonly BankToken[], erc20Tokens?: readonly Erc20Token[], -): types.CosmosSdkTx; +): CosmosSdkTx; diff --git a/packages/cli/README.md b/packages/cli/README.md index e9d512f8..e9bb0653 100644 --- a/packages/cli/README.md +++ b/packages/cli/README.md @@ -50,7 +50,7 @@ const { account_number, sequence } = (await client.authAccounts(faucetAddress)). // Craft a send transaction const emptyAddress = Bech32.encode("cosmos", Random.getBytes(20)); const memo = "My first contract on chain"; -const sendTokensMsg: types.MsgSend = { +const sendTokensMsg: MsgSend = { type: "cosmos-sdk/MsgSend", value: { from_address: faucetAddress, @@ -66,7 +66,7 @@ const sendTokensMsg: types.MsgSend = { const signBytes = makeSignBytes([sendTokensMsg], defaultFee, defaultNetworkId, memo, account_number, sequence); const signature = await pen.sign(signBytes); -const signedTx: types.StdTx = { +const signedTx: StdTx = { msg: [sendTokensMsg], fee: defaultFee, memo: memo, diff --git a/packages/cli/examples/local_faucet.ts b/packages/cli/examples/local_faucet.ts index 21199c9c..77a447b6 100644 --- a/packages/cli/examples/local_faucet.ts +++ b/packages/cli/examples/local_faucet.ts @@ -1,6 +1,6 @@ const defaultHttpUrl = "http://localhost:1317"; const defaultNetworkId = "testing"; -const defaultFee: types.StdFee = { +const defaultFee: StdFee = { amount: [ { amount: "5000", diff --git a/packages/cli/examples/mask.ts b/packages/cli/examples/mask.ts index b1a4f4cb..7a36b0ba 100644 --- a/packages/cli/examples/mask.ts +++ b/packages/cli/examples/mask.ts @@ -5,7 +5,7 @@ export type HandleMsg = msgs: ( | { send: { - amount: types.Coin[]; + amount: Coin[]; from_address: string; to_address: string; }; @@ -15,7 +15,7 @@ export type HandleMsg = contract_addr: string; // this had to be changed - is Base64 encoded string msg: string; - send: types.Coin[] | null; + send: Coin[] | null; }; } | { @@ -53,7 +53,7 @@ export interface State { const base64Msg = (msg: object): string => toBase64(toUtf8(JSON.stringify(msg))); -const sendMsg = (from_address: string, to_address: string, amount: types.Coin[]) => { +const sendMsg = (from_address: string, to_address: string, amount: Coin[]) => { return { send: { from_address, @@ -63,7 +63,7 @@ const sendMsg = (from_address: string, to_address: string, amount: types.Coin[]) }; } -const contractMsg = (contract_addr: string, msg: object, amount?: types.Coin[]) => { +const contractMsg = (contract_addr: string, msg: object, amount?: Coin[]) => { return { contract: { contract_addr, diff --git a/packages/cli/package.json b/packages/cli/package.json index 74e028f4..41231aa9 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -38,7 +38,8 @@ "!**/testdata/" ], "dependencies": { - "@cosmwasm/sdk": "^0.8.0", + "@cosmwasm/cosmwasm": "^0.8.0", + "@cosmwasm/sdk38": "^0.8.0", "@iov/crypto": "^2.1.0", "@iov/encoding": "^2.1.0", "@iov/utils": "^2.0.2", diff --git a/packages/cli/src/cli.ts b/packages/cli/src/cli.ts index d31b0eac..c1bc1bf7 100644 --- a/packages/cli/src/cli.ts +++ b/packages/cli/src/cli.ts @@ -30,19 +30,8 @@ export function main(originalArgs: readonly string[]): void { const imports = new Map([ [ - "@cosmwasm/sdk", + "@cosmwasm/cosmwasm", [ - "encodeSecp256k1Pubkey", - "encodeSecp256k1Signature", - "logs", - "makeCosmoshubPath", - "makeSignBytes", - "marshalTx", - "Pen", - "pubkeyToAddress", - "RestClient", - "Secp256k1Pen", - "types", // cosmwasmclient "Account", "Block", @@ -53,7 +42,6 @@ export function main(originalArgs: readonly string[]): void { "ContractDetails", "CosmWasmClient", "GetNonceResult", - "IndexedTx", "PostTxResult", "SearchByHeightQuery", "SearchByIdQuery", @@ -71,6 +59,30 @@ export function main(originalArgs: readonly string[]): void { "UploadResult", ], ], + [ + "@cosmwasm/sdk38", + [ + "coin", + "coins", + "encodeSecp256k1Pubkey", + "encodeSecp256k1Signature", + "logs", + "makeCosmoshubPath", + "makeSignBytes", + "marshalTx", + "IndexedTx", + "Coin", + "Msg", + "MsgSend", + "Pen", + "PubKey", + "pubkeyToAddress", + "RestClient", + "Secp256k1Pen", + "StdFee", + "StdTx", + ], + ], [ "@iov/crypto", [ diff --git a/packages/sdk/.eslintignore b/packages/cosmwasm/.eslintignore similarity index 100% rename from packages/sdk/.eslintignore rename to packages/cosmwasm/.eslintignore diff --git a/packages/sdk/.gitignore b/packages/cosmwasm/.gitignore similarity index 100% rename from packages/sdk/.gitignore rename to packages/cosmwasm/.gitignore diff --git a/packages/sdk/README.md b/packages/cosmwasm/README.md similarity index 66% rename from packages/sdk/README.md rename to packages/cosmwasm/README.md index 763ef5e6..ab2208fb 100644 --- a/packages/sdk/README.md +++ b/packages/cosmwasm/README.md @@ -1,6 +1,6 @@ -# @cosmwasm/sdk +# @cosmwasm/cosmwasm -[![npm version](https://img.shields.io/npm/v/@cosmwasm/sdk.svg)](https://www.npmjs.com/package/@cosmwasm/sdk) +[![npm version](https://img.shields.io/npm/v/@cosmwasm/cosmwasm.svg)](https://www.npmjs.com/package/@cosmwasm/cosmwasm) An SDK to build CosmWasm clients. diff --git a/packages/sdk/jasmine-testrunner.js b/packages/cosmwasm/jasmine-testrunner.js similarity index 100% rename from packages/sdk/jasmine-testrunner.js rename to packages/cosmwasm/jasmine-testrunner.js diff --git a/packages/sdk/karma.conf.js b/packages/cosmwasm/karma.conf.js similarity index 100% rename from packages/sdk/karma.conf.js rename to packages/cosmwasm/karma.conf.js diff --git a/packages/sdk/nonces/README.txt b/packages/cosmwasm/nonces/README.txt similarity index 100% rename from packages/sdk/nonces/README.txt rename to packages/cosmwasm/nonces/README.txt diff --git a/packages/sdk/package.json b/packages/cosmwasm/package.json similarity index 95% rename from packages/sdk/package.json rename to packages/cosmwasm/package.json index 8b5a4f04..082cca20 100644 --- a/packages/sdk/package.json +++ b/packages/cosmwasm/package.json @@ -1,5 +1,5 @@ { - "name": "@cosmwasm/sdk", + "name": "@cosmwasm/cosmwasm", "version": "0.8.0", "description": "CosmWasm SDK", "author": "Ethan Frey ", @@ -15,7 +15,7 @@ ], "repository": { "type": "git", - "url": "https://github.com/confio/cosmwasm-js/tree/master/packages/sdk" + "url": "https://github.com/confio/cosmwasm-js/tree/master/packages/cosmwasm" }, "publishConfig": { "access": "public" @@ -36,6 +36,7 @@ "pack-web": "yarn build-or-skip && webpack --mode development --config webpack.web.config.js" }, "dependencies": { + "@cosmwasm/sdk38": "^0.8.0", "@iov/crypto": "^2.1.0", "@iov/encoding": "^2.1.0", "@iov/utils": "^2.0.2", diff --git a/packages/sdk/src/builder.spec.ts b/packages/cosmwasm/src/builder.spec.ts similarity index 100% rename from packages/sdk/src/builder.spec.ts rename to packages/cosmwasm/src/builder.spec.ts diff --git a/packages/sdk/src/builder.ts b/packages/cosmwasm/src/builder.ts similarity index 100% rename from packages/sdk/src/builder.ts rename to packages/cosmwasm/src/builder.ts diff --git a/packages/sdk/src/cosmwasmclient.searchtx.spec.ts b/packages/cosmwasm/src/cosmwasmclient.searchtx.spec.ts similarity index 98% rename from packages/sdk/src/cosmwasmclient.searchtx.spec.ts rename to packages/cosmwasm/src/cosmwasmclient.searchtx.spec.ts index cfe91678..e7f981ea 100644 --- a/packages/sdk/src/cosmwasmclient.searchtx.spec.ts +++ b/packages/cosmwasm/src/cosmwasmclient.searchtx.spec.ts @@ -1,10 +1,9 @@ /* eslint-disable @typescript-eslint/camelcase */ +import { Coin, CosmosSdkTx, isMsgSend, makeSignBytes, MsgSend, Secp256k1Pen } from "@cosmwasm/sdk38"; import { assert, sleep } from "@iov/utils"; -import { Coin } from "./coins"; import { CosmWasmClient } from "./cosmwasmclient"; -import { makeSignBytes } from "./encoding"; -import { Secp256k1Pen } from "./pen"; +import { isMsgExecuteContract, isMsgInstantiateContract } from "./msgs"; import { RestClient } from "./restclient"; import { SigningCosmWasmClient } from "./signingcosmwasmclient"; import { @@ -16,7 +15,6 @@ import { wasmd, wasmdEnabled, } from "./testutils.spec"; -import { CosmosSdkTx, isMsgExecuteContract, isMsgInstantiateContract, isMsgSend, MsgSend } from "./types"; describe("CosmWasmClient.searchTx", () => { let sendSuccessful: diff --git a/packages/sdk/src/cosmwasmclient.spec.ts b/packages/cosmwasm/src/cosmwasmclient.spec.ts similarity index 99% rename from packages/sdk/src/cosmwasmclient.spec.ts rename to packages/cosmwasm/src/cosmwasmclient.spec.ts index 2980df89..011e1495 100644 --- a/packages/sdk/src/cosmwasmclient.spec.ts +++ b/packages/cosmwasm/src/cosmwasmclient.spec.ts @@ -1,13 +1,12 @@ /* eslint-disable @typescript-eslint/camelcase */ +import { makeSignBytes, MsgSend, Secp256k1Pen, StdFee } from "@cosmwasm/sdk38"; import { Sha256 } from "@iov/crypto"; import { Bech32, Encoding } from "@iov/encoding"; import { assert, sleep } from "@iov/utils"; import { ReadonlyDate } from "readonly-date"; import { Code, CosmWasmClient, PrivateCosmWasmClient } from "./cosmwasmclient"; -import { makeSignBytes } from "./encoding"; import { findAttribute } from "./logs"; -import { Secp256k1Pen } from "./pen"; import { SigningCosmWasmClient } from "./signingcosmwasmclient"; import cosmoshub from "./testdata/cosmoshub.json"; import { @@ -21,7 +20,6 @@ import { wasmd, wasmdEnabled, } from "./testutils.spec"; -import { MsgSend, StdFee } from "./types"; const { fromHex, fromUtf8, toAscii, toBase64 } = Encoding; diff --git a/packages/sdk/src/cosmwasmclient.ts b/packages/cosmwasm/src/cosmwasmclient.ts similarity index 93% rename from packages/sdk/src/cosmwasmclient.ts rename to packages/cosmwasm/src/cosmwasmclient.ts index 73d81a45..b0c88375 100644 --- a/packages/sdk/src/cosmwasmclient.ts +++ b/packages/cosmwasm/src/cosmwasmclient.ts @@ -1,11 +1,18 @@ +import { + BroadcastMode, + Coin, + CosmosSdkTx, + decodeBech32Pubkey, + IndexedTx, + PubKey, + StdTx, +} from "@cosmwasm/sdk38"; import { Sha256 } from "@iov/crypto"; import { Encoding } from "@iov/encoding"; -import { Coin } from "./coins"; import { Log, parseLogs } from "./logs"; -import { decodeBech32Pubkey } from "./pubkey"; -import { BroadcastMode, RestClient } from "./restclient"; -import { CosmosSdkTx, JsonObject, PubKey, StdTx } from "./types"; +import { RestClient } from "./restclient"; +import { JsonObject } from "./types"; export interface GetNonceResult { readonly accountNumber: number; @@ -103,24 +110,6 @@ export interface ContractDetails extends Contract { readonly initMsg: object; } -/** A transaction that is indexed as part of the transaction history */ -export interface IndexedTx { - readonly height: number; - /** Transaction hash (might be used as transaction ID). Guaranteed to be non-empty upper-case hex */ - readonly hash: string; - /** Transaction execution error code. 0 on success. */ - readonly code: number; - readonly rawLog: string; - readonly logs: readonly Log[]; - readonly tx: CosmosSdkTx; - /** The gas limit as set by the user */ - readonly gasWanted?: number; - /** The gas used by the execution */ - readonly gasUsed?: number; - /** An RFC 3339 time string like e.g. '2020-02-15T10:39:10.4696305Z' */ - readonly timestamp: string; -} - export interface BlockHeader { readonly version: { readonly block: string; diff --git a/packages/cosmwasm/src/index.ts b/packages/cosmwasm/src/index.ts new file mode 100644 index 00000000..53b22ee4 --- /dev/null +++ b/packages/cosmwasm/src/index.ts @@ -0,0 +1,39 @@ +import * as logs from "./logs"; +export { logs }; + +export { RestClient, TxsResponse } from "./restclient"; +export { + Account, + Block, + BlockHeader, + Code, + CodeDetails, + Contract, + ContractDetails, + CosmWasmClient, + GetNonceResult, + PostTxResult, + SearchByHeightQuery, + SearchByIdQuery, + SearchBySentFromOrToQuery, + SearchByTagsQuery, + SearchTxQuery, + SearchTxFilter, +} from "./cosmwasmclient"; +export { + ExecuteResult, + FeeTable, + InstantiateResult, + SigningCallback, + SigningCosmWasmClient, + UploadMeta, + UploadResult, +} from "./signingcosmwasmclient"; +export { + isMsgExecuteContract, + isMsgInstantiateContract, + isMsgStoreCode, + MsgStoreCode, + MsgExecuteContract, + MsgInstantiateContract, +} from "./msgs"; diff --git a/packages/sdk/src/logs.spec.ts b/packages/cosmwasm/src/logs.spec.ts similarity index 100% rename from packages/sdk/src/logs.spec.ts rename to packages/cosmwasm/src/logs.spec.ts diff --git a/packages/sdk/src/logs.ts b/packages/cosmwasm/src/logs.ts similarity index 100% rename from packages/sdk/src/logs.ts rename to packages/cosmwasm/src/logs.ts diff --git a/packages/cosmwasm/src/msgs.ts b/packages/cosmwasm/src/msgs.ts new file mode 100644 index 00000000..e29eeaea --- /dev/null +++ b/packages/cosmwasm/src/msgs.ts @@ -0,0 +1,70 @@ +import { Coin, Msg } from "@cosmwasm/sdk38"; + +/** + * Uploads Wam code to the chain + * + * @see https://github.com/cosmwasm/wasmd/blob/9842678d89/x/wasm/internal/types/msg.go#L17 + */ +export interface MsgStoreCode extends Msg { + readonly type: "wasm/store-code"; + readonly value: { + /** Bech32 account address */ + readonly sender: string; + /** Base64 encoded Wasm */ + readonly wasm_byte_code: string; + /** A valid URI reference to the contract's source code. Can be empty. */ + readonly source: string; + /** A docker tag. Can be empty. */ + readonly builder: string; + }; +} + +/** + * Creates an instance of contract that was uploaded before. + * + * @see https://github.com/cosmwasm/wasmd/blob/9842678d89/x/wasm/internal/types/msg.go#L73 + */ +export interface MsgInstantiateContract extends Msg { + readonly type: "wasm/instantiate"; + readonly value: { + /** Bech32 account address */ + readonly sender: string; + /** ID of the Wasm code that was uploaded before */ + readonly code_id: string; + /** Human-readable label for this contract */ + readonly label: string; + /** Init message as JavaScript object */ + readonly init_msg: any; + readonly init_funds: ReadonlyArray; + }; +} + +/** + * Creates an instance of contract that was uploaded before. + * + * @see https://github.com/cosmwasm/wasmd/blob/9842678d89/x/wasm/internal/types/msg.go#L103 + */ +export interface MsgExecuteContract extends Msg { + readonly type: "wasm/execute"; + readonly value: { + /** Bech32 account address */ + readonly sender: string; + /** Bech32 account address */ + readonly contract: string; + /** Handle message as JavaScript object */ + readonly msg: any; + readonly sent_funds: ReadonlyArray; + }; +} + +export function isMsgStoreCode(msg: Msg): msg is MsgStoreCode { + return (msg as MsgStoreCode).type === "wasm/store-code"; +} + +export function isMsgInstantiateContract(msg: Msg): msg is MsgInstantiateContract { + return (msg as MsgInstantiateContract).type === "wasm/instantiate"; +} + +export function isMsgExecuteContract(msg: Msg): msg is MsgExecuteContract { + return (msg as MsgExecuteContract).type === "wasm/execute"; +} diff --git a/packages/sdk/src/restclient.spec.ts b/packages/cosmwasm/src/restclient.spec.ts similarity index 99% rename from packages/sdk/src/restclient.spec.ts rename to packages/cosmwasm/src/restclient.spec.ts index b65a9d90..1ee42402 100644 --- a/packages/sdk/src/restclient.spec.ts +++ b/packages/cosmwasm/src/restclient.spec.ts @@ -1,16 +1,33 @@ /* eslint-disable @typescript-eslint/camelcase */ +import { + Coin, + encodeBech32Pubkey, + makeCosmoshubPath, + makeSignBytes, + Msg, + MsgSend, + Pen, + PostTxsResponse, + rawSecp256k1PubkeyToAddress, + Secp256k1Pen, + StdFee, + StdSignature, + StdTx, +} from "@cosmwasm/sdk38"; import { Sha256 } from "@iov/crypto"; import { Encoding } from "@iov/encoding"; import { assert, sleep } from "@iov/utils"; import { ReadonlyDate } from "readonly-date"; -import { rawSecp256k1PubkeyToAddress } from "./address"; -import { Coin } from "./coins"; -import { makeSignBytes } from "./encoding"; import { findAttribute, parseLogs } from "./logs"; -import { makeCosmoshubPath, Pen, Secp256k1Pen } from "./pen"; -import { encodeBech32Pubkey } from "./pubkey"; -import { PostTxsResponse, RestClient, TxsResponse } from "./restclient"; +import { + isMsgInstantiateContract, + isMsgStoreCode, + MsgExecuteContract, + MsgInstantiateContract, + MsgStoreCode, +} from "./msgs"; +import { RestClient, TxsResponse } from "./restclient"; import { SigningCosmWasmClient } from "./signingcosmwasmclient"; import cosmoshub from "./testdata/cosmoshub.json"; import { @@ -31,18 +48,6 @@ import { wasmd, wasmdEnabled, } from "./testutils.spec"; -import { - isMsgInstantiateContract, - isMsgStoreCode, - Msg, - MsgExecuteContract, - MsgInstantiateContract, - MsgSend, - MsgStoreCode, - StdFee, - StdSignature, - StdTx, -} from "./types"; const { fromAscii, fromBase64, fromHex, toAscii, toBase64, toHex } = Encoding; diff --git a/packages/cosmwasm/src/restclient.ts b/packages/cosmwasm/src/restclient.ts new file mode 100644 index 00000000..815d12ab --- /dev/null +++ b/packages/cosmwasm/src/restclient.ts @@ -0,0 +1,170 @@ +import { BroadcastMode, CosmosSdkTx, RestClient as BaseRestClient } from "@cosmwasm/sdk38"; +import { Encoding } from "@iov/encoding"; + +import { JsonObject, Model, parseWasmData, WasmData } from "./types"; + +const { fromBase64, fromUtf8, toHex, toUtf8 } = Encoding; + +// 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 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 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; + 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 = ReadonlyArray | null; + +function normalizeArray(backend: CosmosSdkArray): ReadonlyArray { + 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/sdk/src/signingcosmwasmclient.spec.ts b/packages/cosmwasm/src/signingcosmwasmclient.spec.ts similarity index 99% rename from packages/sdk/src/signingcosmwasmclient.spec.ts rename to packages/cosmwasm/src/signingcosmwasmclient.spec.ts index 00fe65eb..753bb76e 100644 --- a/packages/sdk/src/signingcosmwasmclient.spec.ts +++ b/packages/cosmwasm/src/signingcosmwasmclient.spec.ts @@ -1,10 +1,9 @@ +import { Coin, Secp256k1Pen } from "@cosmwasm/sdk38"; import { Sha256 } from "@iov/crypto"; import { Encoding } from "@iov/encoding"; import { assert } from "@iov/utils"; -import { Coin } from "./coins"; import { PrivateCosmWasmClient } from "./cosmwasmclient"; -import { Secp256k1Pen } from "./pen"; import { RestClient } from "./restclient"; import { SigningCosmWasmClient, UploadMeta } from "./signingcosmwasmclient"; import { getHackatom, makeRandomAddress, pendingWithoutWasmd } from "./testutils.spec"; diff --git a/packages/sdk/src/signingcosmwasmclient.ts b/packages/cosmwasm/src/signingcosmwasmclient.ts similarity index 97% rename from packages/sdk/src/signingcosmwasmclient.ts rename to packages/cosmwasm/src/signingcosmwasmclient.ts index e0daf56c..05918010 100644 --- a/packages/sdk/src/signingcosmwasmclient.ts +++ b/packages/cosmwasm/src/signingcosmwasmclient.ts @@ -1,21 +1,12 @@ +import { BroadcastMode, Coin, coins, makeSignBytes, MsgSend, StdFee, StdSignature } from "@cosmwasm/sdk38"; import { Sha256 } from "@iov/crypto"; import { Encoding } from "@iov/encoding"; import pako from "pako"; import { isValidBuilder } from "./builder"; -import { Coin, coins } from "./coins"; import { Account, CosmWasmClient, GetNonceResult, PostTxResult } from "./cosmwasmclient"; -import { makeSignBytes } from "./encoding"; import { findAttribute, Log } from "./logs"; -import { BroadcastMode } from "./restclient"; -import { - MsgExecuteContract, - MsgInstantiateContract, - MsgSend, - MsgStoreCode, - StdFee, - StdSignature, -} from "./types"; +import { MsgExecuteContract, MsgInstantiateContract, MsgStoreCode } from "./msgs"; export interface SigningCallback { (signBytes: Uint8Array): Promise; diff --git a/packages/sdk/src/testdata/contract.json b/packages/cosmwasm/src/testdata/contract.json similarity index 100% rename from packages/sdk/src/testdata/contract.json rename to packages/cosmwasm/src/testdata/contract.json diff --git a/packages/sdk/src/testdata/cosmoshub.json b/packages/cosmwasm/src/testdata/cosmoshub.json similarity index 100% rename from packages/sdk/src/testdata/cosmoshub.json rename to packages/cosmwasm/src/testdata/cosmoshub.json diff --git a/packages/sdk/src/testutils.spec.ts b/packages/cosmwasm/src/testutils.spec.ts similarity index 100% rename from packages/sdk/src/testutils.spec.ts rename to packages/cosmwasm/src/testutils.spec.ts diff --git a/packages/cosmwasm/src/types.ts b/packages/cosmwasm/src/types.ts new file mode 100644 index 00000000..3bcfb2c9 --- /dev/null +++ b/packages/cosmwasm/src/types.ts @@ -0,0 +1,29 @@ +import { Encoding } from "@iov/encoding"; + +const { fromBase64, fromHex } = Encoding; + +export interface WasmData { + // key is hex-encoded + readonly key: string; + // value is base64 encoded + readonly val: string; +} + +// Model is a parsed WasmData object +export interface Model { + readonly key: Uint8Array; + readonly val: Uint8Array; +} + +export function parseWasmData({ key, val }: WasmData): Model { + return { + key: fromHex(key), + val: fromBase64(val), + }; +} + +/** + * An object containing a parsed JSON document. The result of JSON.parse(). + * This doen't privide any type safety over `any` but expresses intent in the code. + */ +export type JsonObject = any; diff --git a/packages/sdk/tsconfig.json b/packages/cosmwasm/tsconfig.json similarity index 100% rename from packages/sdk/tsconfig.json rename to packages/cosmwasm/tsconfig.json diff --git a/packages/sdk/typedoc.js b/packages/cosmwasm/typedoc.js similarity index 100% rename from packages/sdk/typedoc.js rename to packages/cosmwasm/typedoc.js diff --git a/packages/sdk/types/builder.d.ts b/packages/cosmwasm/types/builder.d.ts similarity index 100% rename from packages/sdk/types/builder.d.ts rename to packages/cosmwasm/types/builder.d.ts diff --git a/packages/sdk/types/cosmwasmclient.d.ts b/packages/cosmwasm/types/cosmwasmclient.d.ts similarity index 86% rename from packages/sdk/types/cosmwasmclient.d.ts rename to packages/cosmwasm/types/cosmwasmclient.d.ts index 12ac73ec..15f973e2 100644 --- a/packages/sdk/types/cosmwasmclient.d.ts +++ b/packages/cosmwasm/types/cosmwasmclient.d.ts @@ -1,7 +1,7 @@ -import { Coin } from "./coins"; +import { BroadcastMode, Coin, CosmosSdkTx, IndexedTx, PubKey, StdTx } from "@cosmwasm/sdk38"; import { Log } from "./logs"; -import { BroadcastMode, RestClient } from "./restclient"; -import { CosmosSdkTx, JsonObject, PubKey, StdTx } from "./types"; +import { RestClient } from "./restclient"; +import { JsonObject } from "./types"; export interface GetNonceResult { readonly accountNumber: number; readonly sequence: number; @@ -72,23 +72,6 @@ export interface ContractDetails extends Contract { /** Argument passed on initialization of the contract */ readonly initMsg: object; } -/** A transaction that is indexed as part of the transaction history */ -export interface IndexedTx { - readonly height: number; - /** Transaction hash (might be used as transaction ID). Guaranteed to be non-empty upper-case hex */ - readonly hash: string; - /** Transaction execution error code. 0 on success. */ - readonly code: number; - readonly rawLog: string; - readonly logs: readonly Log[]; - readonly tx: CosmosSdkTx; - /** The gas limit as set by the user */ - readonly gasWanted?: number; - /** The gas used by the execution */ - readonly gasUsed?: number; - /** An RFC 3339 time string like e.g. '2020-02-15T10:39:10.4696305Z' */ - readonly timestamp: string; -} export interface BlockHeader { readonly version: { readonly block: string; diff --git a/packages/cosmwasm/types/index.d.ts b/packages/cosmwasm/types/index.d.ts new file mode 100644 index 00000000..047f950e --- /dev/null +++ b/packages/cosmwasm/types/index.d.ts @@ -0,0 +1,38 @@ +import * as logs from "./logs"; +export { logs }; +export { RestClient, TxsResponse } from "./restclient"; +export { + Account, + Block, + BlockHeader, + Code, + CodeDetails, + Contract, + ContractDetails, + CosmWasmClient, + GetNonceResult, + PostTxResult, + SearchByHeightQuery, + SearchByIdQuery, + SearchBySentFromOrToQuery, + SearchByTagsQuery, + SearchTxQuery, + SearchTxFilter, +} from "./cosmwasmclient"; +export { + ExecuteResult, + FeeTable, + InstantiateResult, + SigningCallback, + SigningCosmWasmClient, + UploadMeta, + UploadResult, +} from "./signingcosmwasmclient"; +export { + isMsgExecuteContract, + isMsgInstantiateContract, + isMsgStoreCode, + MsgStoreCode, + MsgExecuteContract, + MsgInstantiateContract, +} from "./msgs"; diff --git a/packages/sdk/types/logs.d.ts b/packages/cosmwasm/types/logs.d.ts similarity index 100% rename from packages/sdk/types/logs.d.ts rename to packages/cosmwasm/types/logs.d.ts diff --git a/packages/cosmwasm/types/msgs.d.ts b/packages/cosmwasm/types/msgs.d.ts new file mode 100644 index 00000000..ed636a26 --- /dev/null +++ b/packages/cosmwasm/types/msgs.d.ts @@ -0,0 +1,58 @@ +import { Coin, Msg } from "@cosmwasm/sdk38"; +/** + * Uploads Wam code to the chain + * + * @see https://github.com/cosmwasm/wasmd/blob/9842678d89/x/wasm/internal/types/msg.go#L17 + */ +export interface MsgStoreCode extends Msg { + readonly type: "wasm/store-code"; + readonly value: { + /** Bech32 account address */ + readonly sender: string; + /** Base64 encoded Wasm */ + readonly wasm_byte_code: string; + /** A valid URI reference to the contract's source code. Can be empty. */ + readonly source: string; + /** A docker tag. Can be empty. */ + readonly builder: string; + }; +} +/** + * Creates an instance of contract that was uploaded before. + * + * @see https://github.com/cosmwasm/wasmd/blob/9842678d89/x/wasm/internal/types/msg.go#L73 + */ +export interface MsgInstantiateContract extends Msg { + readonly type: "wasm/instantiate"; + readonly value: { + /** Bech32 account address */ + readonly sender: string; + /** ID of the Wasm code that was uploaded before */ + readonly code_id: string; + /** Human-readable label for this contract */ + readonly label: string; + /** Init message as JavaScript object */ + readonly init_msg: any; + readonly init_funds: ReadonlyArray; + }; +} +/** + * Creates an instance of contract that was uploaded before. + * + * @see https://github.com/cosmwasm/wasmd/blob/9842678d89/x/wasm/internal/types/msg.go#L103 + */ +export interface MsgExecuteContract extends Msg { + readonly type: "wasm/execute"; + readonly value: { + /** Bech32 account address */ + readonly sender: string; + /** Bech32 account address */ + readonly contract: string; + /** Handle message as JavaScript object */ + readonly msg: any; + readonly sent_funds: ReadonlyArray; + }; +} +export declare function isMsgStoreCode(msg: Msg): msg is MsgStoreCode; +export declare function isMsgInstantiateContract(msg: Msg): msg is MsgInstantiateContract; +export declare function isMsgExecuteContract(msg: Msg): msg is MsgExecuteContract; diff --git a/packages/cosmwasm/types/restclient.d.ts b/packages/cosmwasm/types/restclient.d.ts new file mode 100644 index 00000000..c5c3d1ee --- /dev/null +++ b/packages/cosmwasm/types/restclient.d.ts @@ -0,0 +1,70 @@ +import { BroadcastMode, CosmosSdkTx, RestClient as BaseRestClient } from "@cosmwasm/sdk38"; +import { JsonObject, Model } from "./types"; +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 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; + 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/sdk/types/signingcosmwasmclient.d.ts b/packages/cosmwasm/types/signingcosmwasmclient.d.ts similarity index 96% rename from packages/sdk/types/signingcosmwasmclient.d.ts rename to packages/cosmwasm/types/signingcosmwasmclient.d.ts index f6c34312..b0c3b280 100644 --- a/packages/sdk/types/signingcosmwasmclient.d.ts +++ b/packages/cosmwasm/types/signingcosmwasmclient.d.ts @@ -1,8 +1,6 @@ -import { Coin } from "./coins"; +import { BroadcastMode, Coin, StdFee, StdSignature } from "@cosmwasm/sdk38"; import { Account, CosmWasmClient, GetNonceResult, PostTxResult } from "./cosmwasmclient"; import { Log } from "./logs"; -import { BroadcastMode } from "./restclient"; -import { StdFee, StdSignature } from "./types"; export interface SigningCallback { (signBytes: Uint8Array): Promise; } diff --git a/packages/cosmwasm/types/types.d.ts b/packages/cosmwasm/types/types.d.ts new file mode 100644 index 00000000..45d870c7 --- /dev/null +++ b/packages/cosmwasm/types/types.d.ts @@ -0,0 +1,14 @@ +export interface WasmData { + readonly key: string; + readonly val: string; +} +export interface Model { + readonly key: Uint8Array; + readonly val: Uint8Array; +} +export declare function parseWasmData({ key, val }: WasmData): Model; +/** + * An object containing a parsed JSON document. The result of JSON.parse(). + * This doen't privide any type safety over `any` but expresses intent in the code. + */ +export declare type JsonObject = any; diff --git a/packages/sdk/webpack.web.config.js b/packages/cosmwasm/webpack.web.config.js similarity index 100% rename from packages/sdk/webpack.web.config.js rename to packages/cosmwasm/webpack.web.config.js diff --git a/packages/demo-staking/package.json b/packages/demo-staking/package.json index 4ea57d57..4d50ff08 100644 --- a/packages/demo-staking/package.json +++ b/packages/demo-staking/package.json @@ -34,7 +34,8 @@ "pack-web": "yarn build-or-skip && webpack --mode development --config webpack.web.config.js" }, "dependencies": { - "@cosmwasm/sdk": "^0.8.0", + "@cosmwasm/cosmwasm": "^0.8.0", + "@cosmwasm/sdk38": "^0.8.0", "@iov/crypto": "^2.1.0", "@iov/encoding": "^2.1.0", "@iov/stream": "^2.0.2", diff --git a/packages/demo-staking/src/index.spec.ts b/packages/demo-staking/src/index.spec.ts index 23a86ad3..1328918f 100644 --- a/packages/demo-staking/src/index.spec.ts +++ b/packages/demo-staking/src/index.spec.ts @@ -1,5 +1,6 @@ /* eslint-disable @typescript-eslint/camelcase */ -import { Coin, coins, makeCosmoshubPath, Secp256k1Pen, SigningCosmWasmClient } from "@cosmwasm/sdk"; +import { SigningCosmWasmClient } from "@cosmwasm/cosmwasm"; +import { Coin, coins, makeCosmoshubPath, Secp256k1Pen } from "@cosmwasm/sdk38"; import { BalanceResponse, diff --git a/packages/faucet/README.md b/packages/faucet/README.md index ec983ca5..3daafd70 100644 --- a/packages/faucet/README.md +++ b/packages/faucet/README.md @@ -1,4 +1,4 @@ -# @cosmwasm/sdk +# @cosmwasm/faucet [![npm version](https://img.shields.io/npm/v/@cosmwasm/faucet.svg)](https://www.npmjs.com/package/@cosmwasm/faucet) diff --git a/packages/sdk/nonces/1580975947 b/packages/sdk/nonces/1580975947 deleted file mode 100644 index e69de29b..00000000 diff --git a/packages/sdk/nonces/1580980830 b/packages/sdk/nonces/1580980830 deleted file mode 100644 index e69de29b..00000000 diff --git a/packages/sdk/nonces/1581790180 b/packages/sdk/nonces/1581790180 deleted file mode 100644 index e69de29b..00000000 diff --git a/packages/sdk/nonces/1582043857 b/packages/sdk/nonces/1582043857 deleted file mode 100644 index e69de29b..00000000 diff --git a/packages/sdk/nonces/1582062520 b/packages/sdk/nonces/1582062520 deleted file mode 100644 index e69de29b..00000000 diff --git a/packages/sdk/nonces/1582198421 b/packages/sdk/nonces/1582198421 deleted file mode 100644 index e69de29b..00000000 diff --git a/packages/sdk/nonces/1582208955 b/packages/sdk/nonces/1582208955 deleted file mode 100644 index e69de29b..00000000 diff --git a/packages/sdk/nonces/1582291410 b/packages/sdk/nonces/1582291410 deleted file mode 100644 index e69de29b..00000000 diff --git a/packages/sdk/nonces/1583183690 b/packages/sdk/nonces/1583183690 deleted file mode 100644 index e69de29b..00000000 diff --git a/packages/sdk/nonces/1583495327 b/packages/sdk/nonces/1583495327 deleted file mode 100644 index e69de29b..00000000 diff --git a/packages/sdk/nonces/1583509277 b/packages/sdk/nonces/1583509277 deleted file mode 100644 index e69de29b..00000000 diff --git a/packages/sdk/nonces/1583541822 b/packages/sdk/nonces/1583541822 deleted file mode 100644 index e69de29b..00000000 diff --git a/packages/sdk/nonces/1583854671 b/packages/sdk/nonces/1583854671 deleted file mode 100644 index e69de29b..00000000 diff --git a/packages/sdk/nonces/1583941462 b/packages/sdk/nonces/1583941462 deleted file mode 100644 index e69de29b..00000000 diff --git a/packages/sdk/nonces/1584541301 b/packages/sdk/nonces/1584541301 deleted file mode 100644 index e69de29b..00000000 diff --git a/packages/sdk/nonces/1584959328 b/packages/sdk/nonces/1584959328 deleted file mode 100644 index e69de29b..00000000 diff --git a/packages/sdk/nonces/1589983882 b/packages/sdk/nonces/1589983882 deleted file mode 100644 index e69de29b..00000000 diff --git a/packages/sdk/nonces/1589986013 b/packages/sdk/nonces/1589986013 deleted file mode 100644 index e69de29b..00000000 diff --git a/packages/sdk/nonces/1590408566 b/packages/sdk/nonces/1590408566 deleted file mode 100644 index e69de29b..00000000 diff --git a/packages/sdk/src/types.ts b/packages/sdk/src/types.ts deleted file mode 100644 index 0ab70b02..00000000 --- a/packages/sdk/src/types.ts +++ /dev/null @@ -1,174 +0,0 @@ -import { Encoding } from "@iov/encoding"; - -import { Coin } from "./coins"; - -const { fromBase64, fromHex } = Encoding; - -/** An Amino/Cosmos SDK StdTx */ -export interface StdTx { - readonly msg: ReadonlyArray; - readonly fee: StdFee; - readonly signatures: ReadonlyArray; - readonly memo: string | undefined; -} - -export function isStdTx(txValue: unknown): txValue is StdTx { - const { memo, msg, fee, signatures } = txValue as StdTx; - return ( - typeof memo === "string" && Array.isArray(msg) && typeof fee === "object" && Array.isArray(signatures) - ); -} - -export interface CosmosSdkTx { - readonly type: string; - readonly value: StdTx; -} - -interface MsgTemplate { - readonly type: string; - readonly value: any; -} - -/** A Cosmos SDK token transfer message */ -export interface MsgSend extends MsgTemplate { - readonly type: "cosmos-sdk/MsgSend"; - readonly value: { - /** Bech32 account address */ - readonly from_address: string; - /** Bech32 account address */ - readonly to_address: string; - readonly amount: ReadonlyArray; - }; -} - -/** - * Uploads Wam code to the chain - * - * @see https://github.com/cosmwasm/wasmd/blob/9842678d89/x/wasm/internal/types/msg.go#L17 - */ -export interface MsgStoreCode extends MsgTemplate { - readonly type: "wasm/store-code"; - readonly value: { - /** Bech32 account address */ - readonly sender: string; - /** Base64 encoded Wasm */ - readonly wasm_byte_code: string; - /** A valid URI reference to the contract's source code. Can be empty. */ - readonly source: string; - /** A docker tag. Can be empty. */ - readonly builder: string; - }; -} - -/** - * Creates an instance of contract that was uploaded before. - * - * @see https://github.com/cosmwasm/wasmd/blob/9842678d89/x/wasm/internal/types/msg.go#L73 - */ -export interface MsgInstantiateContract extends MsgTemplate { - readonly type: "wasm/instantiate"; - readonly value: { - /** Bech32 account address */ - readonly sender: string; - /** ID of the Wasm code that was uploaded before */ - readonly code_id: string; - /** Human-readable label for this contract */ - readonly label: string; - /** Init message as JavaScript object */ - readonly init_msg: any; - readonly init_funds: ReadonlyArray; - }; -} - -/** - * Creates an instance of contract that was uploaded before. - * - * @see https://github.com/cosmwasm/wasmd/blob/9842678d89/x/wasm/internal/types/msg.go#L103 - */ -export interface MsgExecuteContract extends MsgTemplate { - readonly type: "wasm/execute"; - readonly value: { - /** Bech32 account address */ - readonly sender: string; - /** Bech32 account address */ - readonly contract: string; - /** Handle message as JavaScript object */ - readonly msg: any; - readonly sent_funds: ReadonlyArray; - }; -} - -export type Msg = MsgSend | MsgStoreCode | MsgInstantiateContract | MsgExecuteContract | MsgTemplate; - -export function isMsgSend(msg: Msg): msg is MsgSend { - return (msg as MsgSend).type === "cosmos-sdk/MsgSend"; -} - -export function isMsgStoreCode(msg: Msg): msg is MsgStoreCode { - return (msg as MsgStoreCode).type === "wasm/store-code"; -} - -export function isMsgInstantiateContract(msg: Msg): msg is MsgInstantiateContract { - return (msg as MsgInstantiateContract).type === "wasm/instantiate"; -} - -export function isMsgExecuteContract(msg: Msg): msg is MsgExecuteContract { - return (msg as MsgExecuteContract).type === "wasm/execute"; -} - -export interface StdFee { - readonly amount: ReadonlyArray; - readonly gas: string; -} - -export interface StdSignature { - readonly pub_key: PubKey; - readonly signature: string; -} - -export interface PubKey { - // type is one of the strings defined in pubkeyTypes - // I don't use a string literal union here as that makes trouble with json test data: - // https://github.com/confio/cosmwasm-js/pull/44#pullrequestreview-353280504 - readonly type: string; - // Value field is base64-encoded in all cases - // Note: if type is Secp256k1, this must contain a COMPRESSED pubkey - to encode from bcp/keycontrol land, you must compress it first - readonly value: string; -} - -export const pubkeyType = { - /** @see https://github.com/tendermint/tendermint/blob/v0.33.0/crypto/ed25519/ed25519.go#L22 */ - secp256k1: "tendermint/PubKeySecp256k1" as const, - /** @see https://github.com/tendermint/tendermint/blob/v0.33.0/crypto/secp256k1/secp256k1.go#L23 */ - ed25519: "tendermint/PubKeyEd25519" as const, - /** @see https://github.com/tendermint/tendermint/blob/v0.33.0/crypto/sr25519/codec.go#L12 */ - sr25519: "tendermint/PubKeySr25519" as const, -}; - -export const pubkeyTypes: readonly string[] = [pubkeyType.secp256k1, pubkeyType.ed25519, pubkeyType.sr25519]; - -export interface WasmData { - // key is hex-encoded - readonly key: string; - // value is base64 encoded - readonly val: string; -} - -// Model is a parsed WasmData object -export interface Model { - readonly key: Uint8Array; - readonly val: Uint8Array; -} - -export function parseWasmData({ key, val }: WasmData): Model { - return { - key: fromHex(key), - val: fromBase64(val), - }; -} - -/** - * An object containing a parsed JSON document. The result of JSON.parse(). - * This doen't privide any type safety over `any` but expresses intent in the code. - */ -export type JsonObject = any; diff --git a/packages/sdk/types/types.d.ts b/packages/sdk/types/types.d.ts deleted file mode 100644 index aeaa640f..00000000 --- a/packages/sdk/types/types.d.ts +++ /dev/null @@ -1,123 +0,0 @@ -import { Coin } from "./coins"; -/** An Amino/Cosmos SDK StdTx */ -export interface StdTx { - readonly msg: ReadonlyArray; - readonly fee: StdFee; - readonly signatures: ReadonlyArray; - readonly memo: string | undefined; -} -export declare function isStdTx(txValue: unknown): txValue is StdTx; -export interface CosmosSdkTx { - readonly type: string; - readonly value: StdTx; -} -interface MsgTemplate { - readonly type: string; - readonly value: any; -} -/** A Cosmos SDK token transfer message */ -export interface MsgSend extends MsgTemplate { - readonly type: "cosmos-sdk/MsgSend"; - readonly value: { - /** Bech32 account address */ - readonly from_address: string; - /** Bech32 account address */ - readonly to_address: string; - readonly amount: ReadonlyArray; - }; -} -/** - * Uploads Wam code to the chain - * - * @see https://github.com/cosmwasm/wasmd/blob/9842678d89/x/wasm/internal/types/msg.go#L17 - */ -export interface MsgStoreCode extends MsgTemplate { - readonly type: "wasm/store-code"; - readonly value: { - /** Bech32 account address */ - readonly sender: string; - /** Base64 encoded Wasm */ - readonly wasm_byte_code: string; - /** A valid URI reference to the contract's source code. Can be empty. */ - readonly source: string; - /** A docker tag. Can be empty. */ - readonly builder: string; - }; -} -/** - * Creates an instance of contract that was uploaded before. - * - * @see https://github.com/cosmwasm/wasmd/blob/9842678d89/x/wasm/internal/types/msg.go#L73 - */ -export interface MsgInstantiateContract extends MsgTemplate { - readonly type: "wasm/instantiate"; - readonly value: { - /** Bech32 account address */ - readonly sender: string; - /** ID of the Wasm code that was uploaded before */ - readonly code_id: string; - /** Human-readable label for this contract */ - readonly label: string; - /** Init message as JavaScript object */ - readonly init_msg: any; - readonly init_funds: ReadonlyArray; - }; -} -/** - * Creates an instance of contract that was uploaded before. - * - * @see https://github.com/cosmwasm/wasmd/blob/9842678d89/x/wasm/internal/types/msg.go#L103 - */ -export interface MsgExecuteContract extends MsgTemplate { - readonly type: "wasm/execute"; - readonly value: { - /** Bech32 account address */ - readonly sender: string; - /** Bech32 account address */ - readonly contract: string; - /** Handle message as JavaScript object */ - readonly msg: any; - readonly sent_funds: ReadonlyArray; - }; -} -export declare type Msg = MsgSend | MsgStoreCode | MsgInstantiateContract | MsgExecuteContract | MsgTemplate; -export declare function isMsgSend(msg: Msg): msg is MsgSend; -export declare function isMsgStoreCode(msg: Msg): msg is MsgStoreCode; -export declare function isMsgInstantiateContract(msg: Msg): msg is MsgInstantiateContract; -export declare function isMsgExecuteContract(msg: Msg): msg is MsgExecuteContract; -export interface StdFee { - readonly amount: ReadonlyArray; - readonly gas: string; -} -export interface StdSignature { - readonly pub_key: PubKey; - readonly signature: string; -} -export interface PubKey { - readonly type: string; - readonly value: string; -} -export declare const pubkeyType: { - /** @see https://github.com/tendermint/tendermint/blob/v0.33.0/crypto/ed25519/ed25519.go#L22 */ - secp256k1: "tendermint/PubKeySecp256k1"; - /** @see https://github.com/tendermint/tendermint/blob/v0.33.0/crypto/secp256k1/secp256k1.go#L23 */ - ed25519: "tendermint/PubKeyEd25519"; - /** @see https://github.com/tendermint/tendermint/blob/v0.33.0/crypto/sr25519/codec.go#L12 */ - sr25519: "tendermint/PubKeySr25519"; -}; -export declare const pubkeyTypes: readonly string[]; -export interface WasmData { - readonly key: string; - readonly val: string; -} -export interface Model { - readonly key: Uint8Array; - readonly val: Uint8Array; -} -export declare function parseWasmData({ key, val }: WasmData): Model; -/** - * An object containing a parsed JSON document. The result of JSON.parse(). - * This doen't privide any type safety over `any` but expresses intent in the code. - */ -export declare type JsonObject = any; -export {}; diff --git a/packages/sdk38/.eslintignore b/packages/sdk38/.eslintignore new file mode 120000 index 00000000..86039baf --- /dev/null +++ b/packages/sdk38/.eslintignore @@ -0,0 +1 @@ +../../.eslintignore \ No newline at end of file diff --git a/packages/sdk38/.gitignore b/packages/sdk38/.gitignore new file mode 100644 index 00000000..68bf3735 --- /dev/null +++ b/packages/sdk38/.gitignore @@ -0,0 +1,3 @@ +build/ +dist/ +docs/ diff --git a/packages/sdk38/README.md b/packages/sdk38/README.md new file mode 100644 index 00000000..3a0e693e --- /dev/null +++ b/packages/sdk38/README.md @@ -0,0 +1,12 @@ +# @cosmwasm/sdk38 + +[![npm version](https://img.shields.io/npm/v/@cosmwasm/sdk38.svg)](https://www.npmjs.com/package/@cosmwasm/sdk38) + +A client library for the Cosmos SDK 0.38. + +## License + +This package is part of the cosmwasm-js repository, licensed under the Apache +License 2.0 (see +[NOTICE](https://github.com/confio/cosmwasm-js/blob/master/NOTICE) and +[LICENSE](https://github.com/confio/cosmwasm-js/blob/master/LICENSE)). diff --git a/packages/sdk38/jasmine-testrunner.js b/packages/sdk38/jasmine-testrunner.js new file mode 100755 index 00000000..9fada59b --- /dev/null +++ b/packages/sdk38/jasmine-testrunner.js @@ -0,0 +1,26 @@ +#!/usr/bin/env node + +require("source-map-support").install(); +const defaultSpecReporterConfig = require("../../jasmine-spec-reporter.config.json"); + +// setup Jasmine +const Jasmine = require("jasmine"); +const jasmine = new Jasmine(); +jasmine.loadConfig({ + spec_dir: "build", + spec_files: ["**/*.spec.js"], + helpers: [], + random: false, + seed: null, + stopSpecOnExpectationFailure: false, +}); +jasmine.jasmine.DEFAULT_TIMEOUT_INTERVAL = 15 * 1000; + +// setup reporter +const { SpecReporter } = require("jasmine-spec-reporter"); +const reporter = new SpecReporter({ ...defaultSpecReporterConfig }); + +// initialize and execute +jasmine.env.clearReporters(); +jasmine.addReporter(reporter); +jasmine.execute(); diff --git a/packages/sdk38/karma.conf.js b/packages/sdk38/karma.conf.js new file mode 100644 index 00000000..e68db403 --- /dev/null +++ b/packages/sdk38/karma.conf.js @@ -0,0 +1,54 @@ +module.exports = function (config) { + config.set({ + // base path that will be used to resolve all patterns (eg. files, exclude) + basePath: ".", + + // frameworks to use + // available frameworks: https://npmjs.org/browse/keyword/karma-adapter + frameworks: ["jasmine"], + + // list of files / patterns to load in the browser + files: ["dist/web/tests.js"], + + client: { + jasmine: { + random: false, + timeoutInterval: 15000, + }, + }, + + // test results reporter to use + // possible values: 'dots', 'progress' + // available reporters: https://npmjs.org/browse/keyword/karma-reporter + reporters: ["progress", "kjhtml"], + + // web server port + port: 9876, + + // enable / disable colors in the output (reporters and logs) + colors: true, + + // level of logging + // possible values: config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG + logLevel: config.LOG_INFO, + + // enable / disable watching file and executing tests whenever any file changes + autoWatch: false, + + // start these browsers + // available browser launchers: https://npmjs.org/browse/keyword/karma-launcher + browsers: ["Firefox"], + + browserNoActivityTimeout: 90000, + + // Keep brower open for debugging. This is overridden by yarn scripts + singleRun: false, + + customLaunchers: { + ChromeHeadlessInsecure: { + base: "ChromeHeadless", + flags: ["--disable-web-security"], + }, + }, + }); +}; diff --git a/packages/sdk38/nonces/README.txt b/packages/sdk38/nonces/README.txt new file mode 100644 index 00000000..092fe732 --- /dev/null +++ b/packages/sdk38/nonces/README.txt @@ -0,0 +1 @@ +Directory used to trigger lerna package updates for all packages diff --git a/packages/sdk38/package.json b/packages/sdk38/package.json new file mode 100644 index 00000000..9b91856d --- /dev/null +++ b/packages/sdk38/package.json @@ -0,0 +1,48 @@ +{ + "name": "@cosmwasm/sdk38", + "version": "0.8.0", + "description": "Utilities for Cosmos SDK 0.38", + "author": "Ethan Frey ", + "license": "Apache-2.0", + "main": "build/index.js", + "types": "types/index.d.ts", + "files": [ + "build/", + "types/", + "*.md", + "!*.spec.*", + "!**/testdata/" + ], + "repository": { + "type": "git", + "url": "https://github.com/CosmWasm/cosmwasm-js/tree/master/packages/sdk38" + }, + "publishConfig": { + "access": "public" + }, + "scripts": { + "docs": "shx rm -rf docs && typedoc --options typedoc.js", + "format": "prettier --write --loglevel warn \"./src/**/*.ts\"", + "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", + "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", + "test-node": "node jasmine-testrunner.js", + "test-firefox": "yarn pack-web && karma start --single-run --browsers Firefox", + "test-chrome": "yarn pack-web && karma start --single-run --browsers ChromeHeadlessInsecure", + "test": "yarn build-or-skip && yarn test-node", + "pack-web": "yarn build-or-skip && webpack --mode development --config webpack.web.config.js" + }, + "dependencies": { + "@iov/crypto": "^2.1.0", + "@iov/encoding": "^2.1.0", + "@iov/utils": "^2.0.2", + "axios": "^0.19.0", + "fast-deep-equal": "^3.1.1" + }, + "devDependencies": { + "readonly-date": "^1.0.0" + } +} diff --git a/packages/sdk/src/address.spec.ts b/packages/sdk38/src/address.spec.ts similarity index 100% rename from packages/sdk/src/address.spec.ts rename to packages/sdk38/src/address.spec.ts diff --git a/packages/sdk/src/address.ts b/packages/sdk38/src/address.ts similarity index 100% rename from packages/sdk/src/address.ts rename to packages/sdk38/src/address.ts diff --git a/packages/sdk/src/coins.ts b/packages/sdk38/src/coins.ts similarity index 100% rename from packages/sdk/src/coins.ts rename to packages/sdk38/src/coins.ts diff --git a/packages/sdk38/src/cosmosclient.searchtx.spec.ts b/packages/sdk38/src/cosmosclient.searchtx.spec.ts new file mode 100644 index 00000000..96fe3040 --- /dev/null +++ b/packages/sdk38/src/cosmosclient.searchtx.spec.ts @@ -0,0 +1,366 @@ +/* eslint-disable @typescript-eslint/camelcase */ +import { assert, sleep } from "@iov/utils"; + +import { Coin } from "./coins"; +import { CosmosClient } from "./cosmosclient"; +import { makeSignBytes } from "./encoding"; +import { Secp256k1Pen } from "./pen"; +import { RestClient } from "./restclient"; +import { SigningCosmosClient } from "./signingcosmosclient"; +import { + faucet, + fromOneElementArray, + makeRandomAddress, + pendingWithoutWasmd, + wasmd, + wasmdEnabled, +} from "./testutils.spec"; +import { CosmosSdkTx, isMsgSend, MsgSend } from "./types"; + +describe("CosmosClient.searchTx", () => { + let sendSuccessful: + | { + readonly sender: string; + readonly recipient: string; + readonly hash: string; + readonly height: number; + readonly tx: CosmosSdkTx; + } + | undefined; + let sendUnsuccessful: + | { + readonly sender: string; + readonly recipient: string; + readonly hash: string; + readonly height: number; + readonly tx: CosmosSdkTx; + } + | 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: Coin = { + denom: "ucosm", + amount: "1234567", + }; + const result = await client.sendTokens(recipient, [transferAmount]); + await sleep(50); // wait until tx is indexed + const txDetails = await new RestClient(wasmd.endpoint).txById(result.transactionHash); + sendSuccessful = { + sender: faucet.address, + recipient: recipient, + hash: result.transactionHash, + height: Number.parseInt(txDetails.height, 10), + tx: txDetails.tx, + }; + } + + { + 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 tx: CosmosSdkTx = { + type: "cosmos-sdk/StdTx", + value: { + msg: [sendMsg], + fee: fee, + memo: memo, + signatures: [signature], + }, + }; + const transactionId = await client.getIdentifier(tx); + const heightBeforeThis = await client.getHeight(); + try { + await client.postTx(tx.value); + } catch (error) { + // postTx() throws on execution failures, which is a questionable design. Ignore for now. + // console.log(error); + } + sendUnsuccessful = { + sender: faucet.address, + recipient: recipient, + hash: transactionId, + height: heightBeforeThis + 1, + tx: tx, + }; + } + } + }); + + describe("with SearchByIdQuery", () => { + it("can search successful tx by ID", async () => { + pendingWithoutWasmd(); + assert(sendSuccessful, "value must be set in beforeAll()"); + const client = new CosmosClient(wasmd.endpoint); + const result = await client.searchTx({ id: sendSuccessful.hash }); + expect(result.length).toEqual(1); + expect(result[0]).toEqual( + jasmine.objectContaining({ + height: sendSuccessful.height, + hash: sendSuccessful.hash, + code: 0, + tx: sendSuccessful.tx, + }), + ); + }); + + it("can search unsuccessful tx by ID", async () => { + pendingWithoutWasmd(); + assert(sendUnsuccessful, "value must be set in beforeAll()"); + const client = new CosmosClient(wasmd.endpoint); + const result = await client.searchTx({ id: sendUnsuccessful.hash }); + expect(result.length).toEqual(1); + expect(result[0]).toEqual( + jasmine.objectContaining({ + height: sendUnsuccessful.height, + hash: sendUnsuccessful.hash, + code: 5, + tx: sendUnsuccessful.tx, + }), + ); + }); + + it("can search by ID (non existent)", async () => { + pendingWithoutWasmd(); + const client = new CosmosClient(wasmd.endpoint); + const nonExistentId = "0000000000000000000000000000000000000000000000000000000000000000"; + const result = await client.searchTx({ id: nonExistentId }); + expect(result.length).toEqual(0); + }); + + it("can search by ID and filter by minHeight", async () => { + pendingWithoutWasmd(); + assert(sendSuccessful); + const client = new CosmosClient(wasmd.endpoint); + const query = { id: sendSuccessful.hash }; + + { + const result = await client.searchTx(query, { minHeight: 0 }); + expect(result.length).toEqual(1); + } + + { + const result = await client.searchTx(query, { minHeight: sendSuccessful.height - 1 }); + expect(result.length).toEqual(1); + } + + { + const result = await client.searchTx(query, { minHeight: sendSuccessful.height }); + expect(result.length).toEqual(1); + } + + { + const result = await client.searchTx(query, { minHeight: sendSuccessful.height + 1 }); + expect(result.length).toEqual(0); + } + }); + }); + + describe("with SearchByHeightQuery", () => { + it("can search successful tx by height", async () => { + pendingWithoutWasmd(); + assert(sendSuccessful, "value must be set in beforeAll()"); + const client = new CosmosClient(wasmd.endpoint); + const result = await client.searchTx({ height: sendSuccessful.height }); + expect(result.length).toEqual(1); + expect(result[0]).toEqual( + jasmine.objectContaining({ + height: sendSuccessful.height, + hash: sendSuccessful.hash, + code: 0, + tx: sendSuccessful.tx, + }), + ); + }); + + it("can search unsuccessful tx by height", async () => { + pendingWithoutWasmd(); + assert(sendUnsuccessful, "value must be set in beforeAll()"); + const client = new CosmosClient(wasmd.endpoint); + const result = await client.searchTx({ height: sendUnsuccessful.height }); + expect(result.length).toEqual(1); + expect(result[0]).toEqual( + jasmine.objectContaining({ + height: sendUnsuccessful.height, + hash: sendUnsuccessful.hash, + code: 5, + tx: sendUnsuccessful.tx, + }), + ); + }); + }); + + describe("with SearchBySentFromOrToQuery", () => { + it("can search by sender", async () => { + pendingWithoutWasmd(); + assert(sendSuccessful, "value must be set in beforeAll()"); + const client = new CosmosClient(wasmd.endpoint); + const results = await client.searchTx({ sentFromOrTo: sendSuccessful.sender }); + expect(results.length).toBeGreaterThanOrEqual(1); + + // Check basic structure of all results + for (const result of results) { + const containsMsgWithSender = !!result.tx.value.msg.find( + (msg) => isMsgSend(msg) && msg.value.from_address == sendSuccessful!.sender, + ); + const containsMsgWithRecipient = !!result.tx.value.msg.find( + (msg) => isMsgSend(msg) && msg.value.to_address === sendSuccessful!.sender, + ); + expect(containsMsgWithSender || containsMsgWithRecipient).toEqual(true); + } + + // Check details of most recent result + expect(results[results.length - 1]).toEqual( + jasmine.objectContaining({ + height: sendSuccessful.height, + hash: sendSuccessful.hash, + tx: sendSuccessful.tx, + }), + ); + }); + + it("can search by recipient", async () => { + pendingWithoutWasmd(); + assert(sendSuccessful, "value must be set in beforeAll()"); + const client = new CosmosClient(wasmd.endpoint); + const results = await client.searchTx({ sentFromOrTo: sendSuccessful.recipient }); + expect(results.length).toBeGreaterThanOrEqual(1); + + // Check basic structure of all results + for (const result of results) { + const msg = fromOneElementArray(result.tx.value.msg); + assert(isMsgSend(msg), `${result.hash} (height ${result.height}) is not a bank send transaction`); + expect( + msg.value.to_address === sendSuccessful.recipient || + msg.value.from_address == sendSuccessful.recipient, + ).toEqual(true); + } + + // Check details of most recent result + expect(results[results.length - 1]).toEqual( + jasmine.objectContaining({ + height: sendSuccessful.height, + hash: sendSuccessful.hash, + tx: sendSuccessful.tx, + }), + ); + }); + + it("can search by recipient and filter by minHeight", async () => { + pendingWithoutWasmd(); + assert(sendSuccessful); + const client = new CosmosClient(wasmd.endpoint); + const query = { sentFromOrTo: sendSuccessful.recipient }; + + { + const result = await client.searchTx(query, { minHeight: 0 }); + expect(result.length).toEqual(1); + } + + { + const result = await client.searchTx(query, { minHeight: sendSuccessful.height - 1 }); + expect(result.length).toEqual(1); + } + + { + const result = await client.searchTx(query, { minHeight: sendSuccessful.height }); + expect(result.length).toEqual(1); + } + + { + const result = await client.searchTx(query, { minHeight: sendSuccessful.height + 1 }); + expect(result.length).toEqual(0); + } + }); + + it("can search by recipient and filter by maxHeight", async () => { + pendingWithoutWasmd(); + assert(sendSuccessful); + const client = new CosmosClient(wasmd.endpoint); + const query = { sentFromOrTo: sendSuccessful.recipient }; + + { + const result = await client.searchTx(query, { maxHeight: 9999999999999 }); + expect(result.length).toEqual(1); + } + + { + const result = await client.searchTx(query, { maxHeight: sendSuccessful.height + 1 }); + expect(result.length).toEqual(1); + } + + { + const result = await client.searchTx(query, { maxHeight: sendSuccessful.height }); + expect(result.length).toEqual(1); + } + + { + const result = await client.searchTx(query, { maxHeight: sendSuccessful.height - 1 }); + expect(result.length).toEqual(0); + } + }); + }); + + describe("with SearchByTagsQuery", () => { + it("can search by transfer.recipient", async () => { + pendingWithoutWasmd(); + assert(sendSuccessful, "value must be set in beforeAll()"); + const client = new CosmosClient(wasmd.endpoint); + const results = await client.searchTx({ + tags: [{ key: "transfer.recipient", value: sendSuccessful.recipient }], + }); + expect(results.length).toBeGreaterThanOrEqual(1); + + // Check basic structure of all results + for (const result of results) { + const msg = fromOneElementArray(result.tx.value.msg); + assert(isMsgSend(msg), `${result.hash} (height ${result.height}) is not a bank send transaction`); + expect(msg.value.to_address).toEqual(sendSuccessful.recipient); + } + + // Check details of most recent result + expect(results[results.length - 1]).toEqual( + jasmine.objectContaining({ + height: sendSuccessful.height, + hash: sendSuccessful.hash, + tx: sendSuccessful.tx, + }), + ); + }); + }); +}); diff --git a/packages/sdk38/src/cosmosclient.spec.ts b/packages/sdk38/src/cosmosclient.spec.ts new file mode 100644 index 00000000..b41ae6f2 --- /dev/null +++ b/packages/sdk38/src/cosmosclient.spec.ts @@ -0,0 +1,235 @@ +/* eslint-disable @typescript-eslint/camelcase */ +import { sleep } from "@iov/utils"; +import { ReadonlyDate } from "readonly-date"; + +import { CosmosClient, PrivateCosmWasmClient } from "./cosmosclient"; +import { makeSignBytes } from "./encoding"; +import { findAttribute } from "./logs"; +import { Secp256k1Pen } from "./pen"; +import cosmoshub from "./testdata/cosmoshub.json"; +import { + faucet, + makeRandomAddress, + pendingWithoutWasmd, + tendermintIdMatcher, + unused, + wasmd, +} from "./testutils.spec"; +import { MsgSend, StdFee } from "./types"; + +const guest = { + address: "cosmos17d0jcz59jf68g52vq38tuuncmwwjk42u6mcxej", +}; + +describe("CosmosClient", () => { + describe("constructor", () => { + it("can be constructed", () => { + const client = new CosmosClient(wasmd.endpoint); + expect(client).toBeTruthy(); + }); + }); + + describe("getChainId", () => { + it("works", async () => { + pendingWithoutWasmd(); + const client = new CosmosClient(wasmd.endpoint); + expect(await client.getChainId()).toEqual(wasmd.chainId); + }); + + it("caches chain ID", async () => { + pendingWithoutWasmd(); + const client = new CosmosClient(wasmd.endpoint); + const openedClient = (client as unknown) as PrivateCosmWasmClient; + const getCodeSpy = spyOn(openedClient.restClient, "nodeInfo").and.callThrough(); + + expect(await client.getChainId()).toEqual(wasmd.chainId); // from network + expect(await client.getChainId()).toEqual(wasmd.chainId); // from cache + + expect(getCodeSpy).toHaveBeenCalledTimes(1); + }); + }); + + describe("getHeight", () => { + it("gets height via last block", async () => { + pendingWithoutWasmd(); + const client = new CosmosClient(wasmd.endpoint); + const openedClient = (client as unknown) as PrivateCosmWasmClient; + const blockLatestSpy = spyOn(openedClient.restClient, "blocksLatest").and.callThrough(); + + const height1 = await client.getHeight(); + expect(height1).toBeGreaterThan(0); + await sleep(1_000); + const height2 = await client.getHeight(); + expect(height2).toEqual(height1 + 1); + + expect(blockLatestSpy).toHaveBeenCalledTimes(2); + }); + + it("gets height via authAccount once an address is known", async () => { + pendingWithoutWasmd(); + 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 height1 = await client.getHeight(); + expect(height1).toBeGreaterThan(0); + + await client.getAccount(guest.address); // warm up the client + + const height2 = await client.getHeight(); + expect(height2).toBeGreaterThan(0); + await sleep(1_000); + const height3 = await client.getHeight(); + expect(height3).toEqual(height2 + 1); + + expect(blockLatestSpy).toHaveBeenCalledTimes(1); + expect(authAccountsSpy).toHaveBeenCalledTimes(3); + }); + }); + + describe("getNonce", () => { + it("works", async () => { + pendingWithoutWasmd(); + const client = new CosmosClient(wasmd.endpoint); + expect(await client.getNonce(unused.address)).toEqual({ + accountNumber: unused.accountNumber, + sequence: unused.sequence, + }); + }); + + it("throws for missing accounts", async () => { + pendingWithoutWasmd(); + const client = new CosmosClient(wasmd.endpoint); + const missing = makeRandomAddress(); + await client.getNonce(missing).then( + () => fail("this must not succeed"), + (error) => expect(error).toMatch(/account does not exist on chain/i), + ); + }); + }); + + describe("getAccount", () => { + it("works", async () => { + pendingWithoutWasmd(); + const client = new CosmosClient(wasmd.endpoint); + expect(await client.getAccount(unused.address)).toEqual({ + address: unused.address, + accountNumber: unused.accountNumber, + sequence: unused.sequence, + pubkey: undefined, + balance: [ + { denom: "ucosm", amount: "1000000000" }, + { denom: "ustake", amount: "1000000000" }, + ], + }); + }); + + it("returns undefined for missing accounts", async () => { + pendingWithoutWasmd(); + const client = new CosmosClient(wasmd.endpoint); + const missing = makeRandomAddress(); + expect(await client.getAccount(missing)).toBeUndefined(); + }); + }); + + describe("getBlock", () => { + it("works for latest block", async () => { + pendingWithoutWasmd(); + const client = new CosmosClient(wasmd.endpoint); + const response = await client.getBlock(); + + // id + expect(response.id).toMatch(tendermintIdMatcher); + + // header + expect(response.header.height).toBeGreaterThanOrEqual(1); + expect(response.header.chainId).toEqual(await client.getChainId()); + expect(new ReadonlyDate(response.header.time).getTime()).toBeLessThan(ReadonlyDate.now()); + expect(new ReadonlyDate(response.header.time).getTime()).toBeGreaterThanOrEqual( + ReadonlyDate.now() - 5_000, + ); + + // txs + expect(Array.isArray(response.txs)).toEqual(true); + }); + + it("works for block by height", async () => { + pendingWithoutWasmd(); + const client = new CosmosClient(wasmd.endpoint); + const height = (await client.getBlock()).header.height; + const response = await client.getBlock(height - 1); + + // id + expect(response.id).toMatch(tendermintIdMatcher); + + // header + expect(response.header.height).toEqual(height - 1); + expect(response.header.chainId).toEqual(await client.getChainId()); + expect(new ReadonlyDate(response.header.time).getTime()).toBeLessThan(ReadonlyDate.now()); + expect(new ReadonlyDate(response.header.time).getTime()).toBeGreaterThanOrEqual( + ReadonlyDate.now() - 5_000, + ); + + // txs + expect(Array.isArray(response.txs)).toEqual(true); + }); + }); + + describe("getIdentifier", () => { + it("works", async () => { + pendingWithoutWasmd(); + const client = new CosmosClient(wasmd.endpoint); + expect(await client.getIdentifier(cosmoshub.tx)).toEqual(cosmoshub.id); + }); + }); + + describe("postTx", () => { + it("works", async () => { + pendingWithoutWasmd(); + const pen = await Secp256k1Pen.fromMnemonic(faucet.mnemonic); + const client = new CosmosClient(wasmd.endpoint); + + const memo = "My first contract on chain"; + const sendMsg: MsgSend = { + type: "cosmos-sdk/MsgSend", + value: { + from_address: faucet.address, + to_address: makeRandomAddress(), + amount: [ + { + denom: "ucosm", + amount: "1234567", + }, + ], + }, + }; + + const fee: StdFee = { + amount: [ + { + amount: "5000", + denom: "ucosm", + }, + ], + gas: "890000", + }; + + const chainId = await client.getChainId(); + const { accountNumber, sequence } = await client.getNonce(faucet.address); + 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 { logs, transactionHash } = await client.postTx(signedTx); + const amountAttr = findAttribute(logs, "transfer", "amount"); + expect(amountAttr.value).toEqual("1234567ucosm"); + expect(transactionHash).toMatch(/^[0-9A-F]{64}$/); + }); + }); +}); diff --git a/packages/sdk38/src/cosmosclient.ts b/packages/sdk38/src/cosmosclient.ts new file mode 100644 index 00000000..5a020430 --- /dev/null +++ b/packages/sdk38/src/cosmosclient.ts @@ -0,0 +1,314 @@ +import { Sha256 } from "@iov/crypto"; +import { Encoding } from "@iov/encoding"; + +import { Coin } from "./coins"; +import { Log, parseLogs } from "./logs"; +import { decodeBech32Pubkey } from "./pubkey"; +import { BroadcastMode, RestClient } from "./restclient"; +import { CosmosSdkTx, PubKey, StdTx } from "./types"; + +export interface GetNonceResult { + readonly accountNumber: number; + readonly sequence: number; +} + +export interface Account { + /** Bech32 account address */ + readonly address: string; + readonly balance: ReadonlyArray; + readonly pubkey: PubKey | undefined; + readonly accountNumber: number; + readonly sequence: number; +} + +export interface PostTxResult { + readonly logs: readonly Log[]; + readonly rawLog: string; + /** Transaction hash (might be used as transaction ID). Guaranteed to be non-empty upper-case hex */ + readonly transactionHash: string; +} + +export interface SearchByIdQuery { + readonly id: string; +} + +export interface SearchByHeightQuery { + readonly height: number; +} + +export interface SearchBySentFromOrToQuery { + readonly sentFromOrTo: string; +} + +/** + * This query type allows you to pass arbitrary key/value pairs to the backend. It is + * more powerful and slightly lower level than the other search options. + */ +export interface SearchByTagsQuery { + readonly tags: readonly { readonly key: string; readonly value: string }[]; +} + +export type SearchTxQuery = + | SearchByIdQuery + | SearchByHeightQuery + | SearchBySentFromOrToQuery + | SearchByTagsQuery; + +function isSearchByIdQuery(query: SearchTxQuery): query is SearchByIdQuery { + return (query as SearchByIdQuery).id !== undefined; +} + +function isSearchByHeightQuery(query: SearchTxQuery): query is SearchByHeightQuery { + return (query as SearchByHeightQuery).height !== undefined; +} + +function isSearchBySentFromOrToQuery(query: SearchTxQuery): query is SearchBySentFromOrToQuery { + return (query as SearchBySentFromOrToQuery).sentFromOrTo !== undefined; +} + +function isSearchByTagsQuery(query: SearchTxQuery): query is SearchByTagsQuery { + return (query as SearchByTagsQuery).tags !== undefined; +} + +export interface SearchTxFilter { + readonly minHeight?: number; + readonly maxHeight?: number; +} + +/** A transaction that is indexed as part of the transaction history */ +export interface IndexedTx { + readonly height: number; + /** Transaction hash (might be used as transaction ID). Guaranteed to be non-empty upper-case hex */ + readonly hash: string; + /** Transaction execution error code. 0 on success. */ + readonly code: number; + readonly rawLog: string; + readonly logs: readonly Log[]; + readonly tx: CosmosSdkTx; + /** The gas limit as set by the user */ + readonly gasWanted?: number; + /** The gas used by the execution */ + readonly gasUsed?: number; + /** An RFC 3339 time string like e.g. '2020-02-15T10:39:10.4696305Z' */ + readonly timestamp: string; +} + +export interface BlockHeader { + readonly version: { + readonly block: string; + readonly app: string; + }; + readonly height: number; + readonly chainId: string; + /** An RFC 3339 time string like e.g. '2020-02-15T10:39:10.4696305Z' */ + readonly time: string; +} + +export interface Block { + /** The ID is a hash of the block header (uppercase hex) */ + readonly id: string; + readonly header: BlockHeader; + /** Array of raw transactions */ + readonly txs: ReadonlyArray; +} + +/** Use for testing only */ +export interface PrivateCosmWasmClient { + readonly restClient: RestClient; +} + +export class CosmosClient { + protected readonly restClient: RestClient; + /** Any address the chain considers valid (valid bech32 with proper prefix) */ + protected anyValidAddress: string | undefined; + + private chainId: string | undefined; + + /** + * Creates a new client to interact with a CosmWasm blockchain. + * + * This instance does a lot of caching. In order to benefit from that you should try to use one instance + * for the lifetime of your application. When switching backends, a new instance must be created. + * + * @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) { + this.restClient = new RestClient(apiUrl, broadcastMode); + } + + public async getChainId(): Promise { + if (!this.chainId) { + const response = await this.restClient.nodeInfo(); + const chainId = response.node_info.network; + if (!chainId) throw new Error("Chain ID must not be empty"); + this.chainId = chainId; + } + + return this.chainId; + } + + public async getHeight(): Promise { + if (this.anyValidAddress) { + const { height } = await this.restClient.authAccounts(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(); + return parseInt(latest.block.header.height, 10); + } + } + + /** + * Returns a 32 byte upper-case hex transaction hash (typically used as the transaction ID) + */ + public async getIdentifier(tx: CosmosSdkTx): Promise { + // We consult the REST API because we don't have a local amino encoder + const bytes = await this.restClient.encodeTx(tx); + const hash = new Sha256(bytes).digest(); + return Encoding.toHex(hash).toUpperCase(); + } + + /** + * Returns account number and sequence. + * + * Throws if the account does not exist on chain. + * + * @param address returns data for this address. When unset, the client's sender adddress is used. + */ + public async getNonce(address: string): Promise { + const account = await this.getAccount(address); + if (!account) { + throw new Error( + "Account does not exist on chain. Send some tokens there before trying to query nonces.", + ); + } + return { + accountNumber: account.accountNumber, + sequence: account.sequence, + }; + } + + public async getAccount(address: string): Promise { + const account = await this.restClient.authAccounts(address); + const value = account.result.value; + if (value.address === "") { + return undefined; + } else { + this.anyValidAddress = value.address; + return { + address: value.address, + balance: value.coins, + pubkey: value.public_key ? decodeBech32Pubkey(value.public_key) : undefined, + accountNumber: value.account_number, + sequence: value.sequence, + }; + } + } + + /** + * Gets block header and meta + * + * @param height The height of the block. If undefined, the latest height is used. + */ + public async getBlock(height?: number): Promise { + const response = + height !== undefined ? await this.restClient.blocks(height) : await this.restClient.blocksLatest(); + + return { + id: response.block_id.hash, + header: { + version: response.block.header.version, + time: response.block.header.time, + height: parseInt(response.block.header.height, 10), + chainId: response.block.header.chain_id, + }, + txs: (response.block.data.txs || []).map((encoded) => Encoding.fromBase64(encoded)), + }; + } + + public async searchTx(query: SearchTxQuery, filter: SearchTxFilter = {}): Promise { + const minHeight = filter.minHeight || 0; + const maxHeight = filter.maxHeight || Number.MAX_SAFE_INTEGER; + + if (maxHeight < minHeight) return []; // optional optimization + + function withFilters(originalQuery: string): string { + return `${originalQuery}&tx.minheight=${minHeight}&tx.maxheight=${maxHeight}`; + } + + let txs: readonly IndexedTx[]; + if (isSearchByIdQuery(query)) { + txs = await this.txsQuery(`tx.hash=${query.id}`); + } else if (isSearchByHeightQuery(query)) { + // optional optimization to avoid network request + if (query.height < minHeight || query.height > maxHeight) { + txs = []; + } else { + txs = await this.txsQuery(`tx.height=${query.height}`); + } + } else if (isSearchBySentFromOrToQuery(query)) { + // We cannot get both in one request (see https://github.com/cosmos/gaia/issues/75) + const sentQuery = withFilters(`message.module=bank&message.sender=${query.sentFromOrTo}`); + const receivedQuery = withFilters(`message.module=bank&transfer.recipient=${query.sentFromOrTo}`); + const sent = await this.txsQuery(sentQuery); + const received = await this.txsQuery(receivedQuery); + + const sentHashes = sent.map((t) => t.hash); + txs = [...sent, ...received.filter((t) => !sentHashes.includes(t.hash))]; + } else if (isSearchByTagsQuery(query)) { + const rawQuery = withFilters(query.tags.map((t) => `${t.key}=${t.value}`).join("&")); + txs = await this.txsQuery(rawQuery); + } else { + throw new Error("Unknown query type"); + } + + // backend sometimes messes up with min/max height filtering + const filtered = txs.filter((tx) => tx.height >= minHeight && tx.height <= maxHeight); + + return filtered; + } + + public async postTx(tx: StdTx): Promise { + const result = await this.restClient.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"); + } + + if (result.code) { + throw new Error( + `Error when posting tx ${result.txhash}. Code: ${result.code}; Raw log: ${result.raw_log}`, + ); + } + + return { + logs: result.logs ? parseLogs(result.logs) : [], + rawLog: result.raw_log || "", + transactionHash: result.txhash, + }; + } + + 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 pages = parseInt(result.page_total, 10); + if (pages > 1) { + throw new Error( + `Found more results on the backend than we can process currently. Results: ${result.total_count}, supported: ${limit}`, + ); + } + return result.txs.map( + (restItem): IndexedTx => ({ + height: parseInt(restItem.height, 10), + hash: restItem.txhash, + code: restItem.code || 0, + rawLog: restItem.raw_log, + logs: parseLogs(restItem.logs || []), + tx: restItem.tx, + timestamp: restItem.timestamp, + }), + ); + } +} diff --git a/packages/sdk/src/decoding.ts b/packages/sdk38/src/decoding.ts similarity index 100% rename from packages/sdk/src/decoding.ts rename to packages/sdk38/src/decoding.ts diff --git a/packages/sdk/src/encoding.spec.ts b/packages/sdk38/src/encoding.spec.ts similarity index 100% rename from packages/sdk/src/encoding.spec.ts rename to packages/sdk38/src/encoding.spec.ts diff --git a/packages/sdk/src/encoding.ts b/packages/sdk38/src/encoding.ts similarity index 100% rename from packages/sdk/src/encoding.ts rename to packages/sdk38/src/encoding.ts diff --git a/packages/sdk/src/index.ts b/packages/sdk38/src/index.ts similarity index 50% rename from packages/sdk/src/index.ts rename to packages/sdk38/src/index.ts index 5162bdab..f0cfeafb 100644 --- a/packages/sdk/src/index.ts +++ b/packages/sdk38/src/index.ts @@ -1,21 +1,14 @@ import * as logs from "./logs"; -import * as types from "./types"; -export { logs, types }; +export { logs }; -export { pubkeyToAddress } from "./address"; +export { pubkeyToAddress, rawSecp256k1PubkeyToAddress } from "./address"; export { Coin, coin, coins } from "./coins"; -export { unmarshalTx } from "./decoding"; -export { makeSignBytes, marshalTx } from "./encoding"; -export { BroadcastMode, RestClient, TxsResponse } from "./restclient"; + export { Account, Block, BlockHeader, - Code, - CodeDetails, - Contract, - ContractDetails, - CosmWasmClient, + CosmosClient, GetNonceResult, IndexedTx, PostTxResult, @@ -25,17 +18,33 @@ export { SearchByTagsQuery, SearchTxQuery, SearchTxFilter, -} from "./cosmwasmclient"; -export { makeCosmoshubPath, Pen, PrehashType, Secp256k1Pen } from "./pen"; +} from "./cosmosclient"; +export { unmarshalTx } from "./decoding"; +export { makeSignBytes, marshalTx } from "./encoding"; +export { + AuthAccountsResponse, + BlockResponse, + BroadcastMode, + PostTxsResponse, + NodeInfoResponse, + RestClient, + SearchTxsResponse, + TxsResponse, +} from "./restclient"; +export { Pen, Secp256k1Pen, makeCosmoshubPath } from "./pen"; export { decodeBech32Pubkey, encodeBech32Pubkey, encodeSecp256k1Pubkey } from "./pubkey"; export { findSequenceForSignedTx } from "./sequence"; export { encodeSecp256k1Signature, decodeSignature } from "./signature"; +export { FeeTable, SigningCallback, SigningCosmosClient } from "./signingcosmosclient"; export { - ExecuteResult, - FeeTable, - InstantiateResult, - SigningCallback, - SigningCosmWasmClient, - UploadMeta, - UploadResult, -} from "./signingcosmwasmclient"; + isMsgSend, + isStdTx, + pubkeyType, + CosmosSdkTx, + PubKey, + Msg, + MsgSend, + StdFee, + StdSignature, + StdTx, +} from "./types"; diff --git a/packages/sdk38/src/logs.spec.ts b/packages/sdk38/src/logs.spec.ts new file mode 100644 index 00000000..584d9ef2 --- /dev/null +++ b/packages/sdk38/src/logs.spec.ts @@ -0,0 +1,165 @@ +/* eslint-disable @typescript-eslint/camelcase */ +import { parseAttribute, parseEvent, parseLog, parseLogs } from "./logs"; + +describe("logs", () => { + describe("parseAttribute", () => { + it("works", () => { + const attr = parseAttribute({ key: "a", value: "b" }); + expect(attr).toEqual({ key: "a", value: "b" }); + }); + + it("works for empty value", () => { + const attr = parseAttribute({ key: "foobar", value: "" }); + expect(attr).toEqual({ key: "foobar", value: "" }); + }); + + it("normalized unset value to empty string", () => { + const attr = parseAttribute({ key: "amount" }); + expect(attr).toEqual({ key: "amount", value: "" }); + }); + }); + + describe("parseEvent", () => { + it("works", () => { + const original = { + type: "message", + attributes: [ + { + key: "action", + value: "store-code", + }, + { + key: "module", + value: "wasm", + }, + { + key: "action", + value: "store-code", + }, + { + key: "sender", + value: "cosmos1pkptre7fdkl6gfrzlesjjvhxhlc3r4gmmk8rs6", + }, + { + key: "code_id", + value: "1", + }, + ], + } as const; + + const event = parseEvent(original); + expect(event).toEqual(original); + }); + + it("works for transfer event", () => { + const original = { + type: "transfer", + attributes: [ + { + key: "recipient", + value: "cosmos18vd8fpwxzck93qlwghaj6arh4p7c5n89uzcee5", + }, + { + key: "amount", + }, + ], + } as const; + const expected = { + type: "transfer", + attributes: [ + { + key: "recipient", + value: "cosmos18vd8fpwxzck93qlwghaj6arh4p7c5n89uzcee5", + }, + { + key: "amount", + value: "", + }, + ], + } as const; + + const event = parseEvent(original); + expect(event).toEqual(expected); + }); + }); + + describe("parseLog", () => { + it("works", () => { + const original = { + msg_index: 0, + log: "", + events: [ + { + type: "message", + attributes: [ + { + key: "action", + value: "store-code", + }, + { + key: "module", + value: "wasm", + }, + { + key: "action", + value: "store-code", + }, + { + key: "sender", + value: "cosmos1pkptre7fdkl6gfrzlesjjvhxhlc3r4gmmk8rs6", + }, + { + key: "code_id", + value: "1", + }, + ], + }, + ], + } as const; + + const log = parseLog(original); + expect(log).toEqual(original); + }); + }); + + describe("parseLogs", () => { + it("works", () => { + const original = [ + { + msg_index: 0, + log: "", + events: [ + { + type: "message", + attributes: [ + { + key: "action", + value: "store-code", + }, + { + key: "module", + value: "wasm", + }, + { + key: "action", + value: "store-code", + }, + { + key: "sender", + value: "cosmos1pkptre7fdkl6gfrzlesjjvhxhlc3r4gmmk8rs6", + }, + { + key: "code_id", + value: "1", + }, + ], + }, + ], + }, + ] as const; + + const logs = parseLogs(original); + expect(logs).toEqual(original); + }); + }); +}); diff --git a/packages/sdk38/src/logs.ts b/packages/sdk38/src/logs.ts new file mode 100644 index 00000000..e1eaa1cb --- /dev/null +++ b/packages/sdk38/src/logs.ts @@ -0,0 +1,86 @@ +/* eslint-disable @typescript-eslint/camelcase */ +import { isNonNullObject } from "@iov/encoding"; + +export interface Attribute { + readonly key: string; + readonly value: string; +} + +export interface Event { + readonly type: string; + readonly attributes: readonly Attribute[]; +} + +export interface Log { + readonly msg_index: number; + readonly log: string; + readonly events: readonly Event[]; +} + +export function parseAttribute(input: unknown): Attribute { + if (!isNonNullObject(input)) throw new Error("Attribute must be a non-null object"); + const { key, value } = input as any; + if (typeof key !== "string" || !key) throw new Error("Attribute's key must be a non-empty string"); + if (typeof value !== "string" && typeof value !== "undefined") { + throw new Error("Attribute's value must be a string or unset"); + } + + return { + key: key, + value: value || "", + }; +} + +export function parseEvent(input: unknown): Event { + if (!isNonNullObject(input)) throw new Error("Event must be a non-null object"); + const { type, attributes } = input as any; + if (typeof type !== "string" || type === "") { + throw new Error(`Event type must be a non-empty string`); + } + if (!Array.isArray(attributes)) throw new Error("Event's attributes must be an array"); + return { + type: type, + attributes: attributes.map(parseAttribute), + }; +} + +export function parseLog(input: unknown): Log { + if (!isNonNullObject(input)) throw new Error("Log must be a non-null object"); + const { msg_index, log, events } = input as any; + if (typeof msg_index !== "number") throw new Error("Log's msg_index must be a number"); + if (typeof log !== "string") throw new Error("Log's log must be a string"); + if (!Array.isArray(events)) throw new Error("Log's events must be an array"); + return { + msg_index: msg_index, + log: log, + events: events.map(parseEvent), + }; +} + +export function parseLogs(input: unknown): readonly Log[] { + if (!Array.isArray(input)) throw new Error("Logs must be an array"); + return input.map(parseLog); +} + +/** + * Searches in logs for the first event of the given event type and in that event + * for the first first attribute with the given attribute key. + * + * Throws if the attribute was not found. + */ +export function findAttribute( + logs: readonly Log[], + eventType: "message" | "transfer", + attrKey: string, +): Attribute { + const firstLogs = logs.find(() => true); + const out = firstLogs?.events + .find((event) => event.type === eventType) + ?.attributes.find((attr) => attr.key === attrKey); + if (!out) { + throw new Error( + `Could not find attribute '${attrKey}' in first event of type '${eventType}' in first log.`, + ); + } + return out; +} diff --git a/packages/sdk/src/pen.spec.ts b/packages/sdk38/src/pen.spec.ts similarity index 100% rename from packages/sdk/src/pen.spec.ts rename to packages/sdk38/src/pen.spec.ts diff --git a/packages/sdk/src/pen.ts b/packages/sdk38/src/pen.ts similarity index 100% rename from packages/sdk/src/pen.ts rename to packages/sdk38/src/pen.ts diff --git a/packages/sdk/src/pubkey.spec.ts b/packages/sdk38/src/pubkey.spec.ts similarity index 100% rename from packages/sdk/src/pubkey.spec.ts rename to packages/sdk38/src/pubkey.spec.ts diff --git a/packages/sdk/src/pubkey.ts b/packages/sdk38/src/pubkey.ts similarity index 100% rename from packages/sdk/src/pubkey.ts rename to packages/sdk38/src/pubkey.ts diff --git a/packages/sdk38/src/restclient.spec.ts b/packages/sdk38/src/restclient.spec.ts new file mode 100644 index 00000000..7dc391a4 --- /dev/null +++ b/packages/sdk38/src/restclient.spec.ts @@ -0,0 +1,897 @@ +/* eslint-disable @typescript-eslint/camelcase */ +import { Encoding } from "@iov/encoding"; +import { assert, sleep } from "@iov/utils"; +import { ReadonlyDate } from "readonly-date"; + +import { rawSecp256k1PubkeyToAddress } from "./address"; +import { makeSignBytes } from "./encoding"; +import { parseLogs } from "./logs"; +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 { + faucet, + makeRandomAddress, + nonNegativeIntegerMatcher, + pendingWithoutWasmd, + semverMatcher, + tendermintAddressMatcher, + tendermintIdMatcher, + tendermintOptionalIdMatcher, + tendermintShortHashMatcher, + unused, + wasmd, + wasmdEnabled, +} from "./testutils.spec"; +import { Msg, MsgSend, StdFee, StdSignature, StdTx } from "./types"; + +const { fromBase64 } = Encoding; + +const emptyAddress = "cosmos1ltkhnmdcqemmd2tkhnx7qx66tq7e0wykw2j85k"; + +function makeSignedTx(firstMsg: Msg, fee: StdFee, memo: string, firstSignature: StdSignature): StdTx { + return { + msg: [firstMsg], + fee: fee, + memo: memo, + signatures: [firstSignature], + }; +} + +describe("RestClient", () => { + 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", + 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 }); + try { + await client.postTx(signedTx); + } catch (error) { + // postTx() throws on execution failures, which is a questionable design. Ignore for now. + // console.log(error); + } + unsuccessful = { + sender: faucet.address, + recipient: recipient, + hash: transactionId, + }; + } + + await sleep(50); // 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(50); // 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: "1", + limit: "26", + page_number: "1", + page_total: "1", + total_count: "1", + txs: [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); + expect(await client.encodeTx(cosmoshub.tx)).toEqual(fromBase64(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: emptyAddress, + 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: emptyAddress, + 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); + // console.log(postResult.raw_log); + 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: emptyAddress, + amount: [ + { + denom: "ucosm", + amount: "1234567", + }, + ], + }, + }; + const msg2: MsgSend = { + type: "cosmos-sdk/MsgSend", + value: { + from_address: address1, + to_address: emptyAddress, + 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); + // console.log(postResult.raw_log); + 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: emptyAddress, + amount: [ + { + denom: "ucosm", + amount: "1234567", + }, + ], + }, + }; + const msg2: MsgSend = { + type: "cosmos-sdk/MsgSend", + value: { + from_address: address2, + to_address: emptyAddress, + 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); + // console.log(postResult.raw_log); + 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: emptyAddress, + amount: [ + { + denom: "ucosm", + amount: "1234567", + }, + ], + }, + }; + const msg2: MsgSend = { + type: "cosmos-sdk/MsgSend", + value: { + from_address: address2, + to_address: emptyAddress, + 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); + // console.log(postResult.raw_log); + 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: emptyAddress, + amount: [ + { + denom: "ucosm", + amount: "1234567", + }, + ], + }, + }; + const msg2: MsgSend = { + type: "cosmos-sdk/MsgSend", + value: { + from_address: address2, + to_address: emptyAddress, + 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); + // console.log(postResult.raw_log); + expect(postResult.code).toEqual(8); + }); + }); +}); diff --git a/packages/sdk/src/restclient.ts b/packages/sdk38/src/restclient.ts similarity index 61% rename from packages/sdk/src/restclient.ts rename to packages/sdk38/src/restclient.ts index cb6316e9..85afb05f 100644 --- a/packages/sdk/src/restclient.ts +++ b/packages/sdk38/src/restclient.ts @@ -2,9 +2,7 @@ import { Encoding, isNonNullObject } from "@iov/encoding"; import axios, { AxiosError, AxiosInstance } from "axios"; import { Coin } from "./coins"; -import { CosmosSdkTx, JsonObject, Model, parseWasmData, StdTx, WasmData } from "./types"; - -const { fromBase64, fromUtf8, toHex, toUtf8 } = Encoding; +import { CosmosSdkTx, StdTx } from "./types"; export interface CosmosSdkAccount { /** Bech32 account address */ @@ -16,7 +14,7 @@ export interface CosmosSdkAccount { readonly sequence: number; } -export interface NodeInfo { +interface NodeInfo { readonly protocol_version: { readonly p2p: string; readonly block: string; @@ -34,7 +32,7 @@ export interface NodeInfo { }; } -export interface ApplicationVersion { +interface ApplicationVersion { readonly name: string; readonly server_name: string; readonly client_name: string; @@ -49,7 +47,7 @@ export interface NodeInfoResponse { readonly application_version: ApplicationVersion; } -export interface BlockId { +interface BlockId { readonly hash: string; // TODO: here we also have this // parts: { @@ -58,7 +56,7 @@ export interface BlockId { // } } -export interface BlockHeader { +interface BlockHeader { readonly version: { readonly block: string; readonly app: string; @@ -82,7 +80,7 @@ export interface BlockHeader { readonly proposer_address: string; } -export interface Block { +interface Block { readonly header: BlockHeader; readonly data: { /** Array of base64 encoded transactions */ @@ -95,7 +93,7 @@ export interface BlockResponse { readonly block: Block; } -interface AuthAccountsResponse { +export interface AuthAccountsResponse { readonly height: string; readonly result: { readonly type: "cosmos-sdk/Account"; @@ -134,7 +132,7 @@ export interface TxsResponse { readonly timestamp: string; } -interface SearchTxsResponse { +export interface SearchTxsResponse { readonly total_count: string; readonly count: string; readonly page_number: string; @@ -161,62 +159,6 @@ interface EncodeTxResponse { readonly tx: 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; - 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; -} - -type RestClientResponse = - | NodeInfoResponse - | BlockResponse - | AuthAccountsResponse - | TxsResponse - | SearchTxsResponse - | PostTxsResponse - | EncodeTxResponse - | WasmResponse - | WasmResponse - | WasmResponse - | WasmResponse - | WasmResponse; - -/** Unfortunately, Cosmos SDK encodes empty arrays as null */ -type CosmosSdkArray = ReadonlyArray | null; - -function normalizeArray(backend: CosmosSdkArray): ReadonlyArray { - return backend || []; -} - /** * The mode used to send transaction * @@ -231,17 +173,6 @@ export enum BroadcastMode { Async = "async", } -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; -} - // 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 @@ -290,7 +221,7 @@ export class RestClient { this.broadcastMode = broadcastMode; } - public async get(path: string): Promise { + 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"); @@ -298,7 +229,7 @@ export class RestClient { return data; } - public async post(path: string, params: any): Promise { + 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) { @@ -312,7 +243,7 @@ export class RestClient { public async authAccounts(address: string): Promise { const path = `/auth/accounts/${address}`; const responseData = await this.get(path); - if ((responseData as any).result.type !== "cosmos-sdk/Account") { + if (responseData.result.type !== "cosmos-sdk/Account") { throw new Error("Unexpected response data format"); } return responseData as AuthAccountsResponse; @@ -322,7 +253,7 @@ export class RestClient { public async blocksLatest(): Promise { const responseData = await this.get("/blocks/latest"); - if (!(responseData as any).block) { + if (!responseData.block) { throw new Error("Unexpected response data format"); } return responseData as BlockResponse; @@ -330,7 +261,7 @@ export class RestClient { public async blocks(height: number): Promise { const responseData = await this.get(`/blocks/${height}`); - if (!(responseData as any).block) { + if (!responseData.block) { throw new Error("Unexpected response data format"); } return responseData as BlockResponse; @@ -340,7 +271,7 @@ export class RestClient { public async nodeInfo(): Promise { const responseData = await this.get("/node_info"); - if (!(responseData as any).node_info) { + if (!responseData.node_info) { throw new Error("Unexpected response data format"); } return responseData as NodeInfoResponse; @@ -350,7 +281,7 @@ export class RestClient { public async txById(id: string): Promise { const responseData = await this.get(`/txs/${id}`); - if (!(responseData as any).tx) { + if (!responseData.tx) { throw new Error("Unexpected response data format"); } return responseData as TxsResponse; @@ -358,7 +289,7 @@ export class RestClient { public async txsQuery(query: string): Promise { const responseData = await this.get(`/txs?${query}`); - if (!(responseData as any).txs) { + if (!responseData.txs) { throw new Error("Unexpected response data format"); } return responseData as SearchTxsResponse; @@ -367,7 +298,7 @@ export class RestClient { /** 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 as any).tx) { + if (!responseData.tx) { throw new Error("Unexpected response data format"); } return Encoding.fromBase64((responseData as EncodeTxResponse).tx); @@ -386,72 +317,9 @@ export class RestClient { mode: this.broadcastMode, }; const responseData = await this.post("/txs", params); - if (!(responseData as any).txhash) { + if (!responseData.txhash) { throw new Error("Unexpected response data format"); } return responseData as PostTxsResponse; } - - // 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/sdk/src/sequence.spec.ts b/packages/sdk38/src/sequence.spec.ts similarity index 100% rename from packages/sdk/src/sequence.spec.ts rename to packages/sdk38/src/sequence.spec.ts diff --git a/packages/sdk/src/sequence.ts b/packages/sdk38/src/sequence.ts similarity index 100% rename from packages/sdk/src/sequence.ts rename to packages/sdk38/src/sequence.ts diff --git a/packages/sdk/src/signature.spec.ts b/packages/sdk38/src/signature.spec.ts similarity index 100% rename from packages/sdk/src/signature.spec.ts rename to packages/sdk38/src/signature.spec.ts diff --git a/packages/sdk/src/signature.ts b/packages/sdk38/src/signature.ts similarity index 100% rename from packages/sdk/src/signature.ts rename to packages/sdk38/src/signature.ts diff --git a/packages/sdk38/src/signingcosmosclient.spec.ts b/packages/sdk38/src/signingcosmosclient.spec.ts new file mode 100644 index 00000000..5054d925 --- /dev/null +++ b/packages/sdk38/src/signingcosmosclient.spec.ts @@ -0,0 +1,78 @@ +import { assert } from "@iov/utils"; + +import { Coin } from "./coins"; +import { PrivateCosmWasmClient } from "./cosmosclient"; +import { Secp256k1Pen } from "./pen"; +import { SigningCosmosClient } from "./signingcosmosclient"; +import { makeRandomAddress, pendingWithoutWasmd } from "./testutils.spec"; + +const httpUrl = "http://localhost:1317"; + +const faucet = { + mnemonic: + "economy stock theory fatal elder harbor betray wasp final emotion task crumble siren bottom lizard educate guess current outdoor pair theory focus wife stone", + pubkey: { + type: "tendermint/PubKeySecp256k1", + value: "A08EGB7ro1ORuFhjOnZcSgwYlpe0DSFjVNUIkNNQxwKQ", + }, + address: "cosmos1pkptre7fdkl6gfrzlesjjvhxhlc3r4gmmk8rs6", +}; + +describe("SigningCosmosClient", () => { + describe("makeReadOnly", () => { + it("can be constructed", async () => { + const pen = await Secp256k1Pen.fromMnemonic(faucet.mnemonic); + const client = new SigningCosmosClient(httpUrl, faucet.address, (signBytes) => pen.sign(signBytes)); + expect(client).toBeTruthy(); + }); + }); + + describe("getHeight", () => { + it("always uses authAccount implementation", async () => { + pendingWithoutWasmd(); + const pen = await Secp256k1Pen.fromMnemonic(faucet.mnemonic); + 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 height = await client.getHeight(); + expect(height).toBeGreaterThan(0); + + expect(blockLatestSpy).toHaveBeenCalledTimes(0); + expect(authAccountsSpy).toHaveBeenCalledTimes(1); + }); + }); + + describe("sendTokens", () => { + it("works", async () => { + pendingWithoutWasmd(); + const pen = await Secp256k1Pen.fromMnemonic(faucet.mnemonic); + const client = new SigningCosmosClient(httpUrl, faucet.address, (signBytes) => pen.sign(signBytes)); + + // instantiate + const transferAmount: readonly Coin[] = [ + { + amount: "7890", + denom: "ucosm", + }, + ]; + const beneficiaryAddress = makeRandomAddress(); + + // no tokens here + const before = await client.getAccount(beneficiaryAddress); + expect(before).toBeUndefined(); + + // send + const result = await client.sendTokens(beneficiaryAddress, transferAmount, "for dinner"); + const [firstLog] = result.logs; + expect(firstLog).toBeTruthy(); + + // got tokens + const after = await client.getAccount(beneficiaryAddress); + assert(after); + expect(after.balance).toEqual(transferAmount); + }); + }); +}); diff --git a/packages/sdk38/src/signingcosmosclient.ts b/packages/sdk38/src/signingcosmosclient.ts new file mode 100644 index 00000000..c144955e --- /dev/null +++ b/packages/sdk38/src/signingcosmosclient.ts @@ -0,0 +1,107 @@ +import { Coin, coins } from "./coins"; +import { Account, CosmosClient, GetNonceResult, PostTxResult } from "./cosmosclient"; +import { makeSignBytes } from "./encoding"; +import { BroadcastMode } from "./restclient"; +import { MsgSend, StdFee, StdSignature } from "./types"; + +export interface SigningCallback { + (signBytes: Uint8Array): Promise; +} + +export interface FeeTable { + readonly upload: StdFee; + readonly init: StdFee; + readonly exec: StdFee; + readonly send: StdFee; +} + +const defaultFees: FeeTable = { + upload: { + amount: coins(25000, "ucosm"), + gas: "1000000", // one million + }, + init: { + amount: coins(12500, "ucosm"), + gas: "500000", // 500k + }, + exec: { + amount: coins(5000, "ucosm"), + gas: "200000", // 200k + }, + send: { + amount: coins(2000, "ucosm"), + gas: "80000", // 80k + }, +}; + +export class SigningCosmosClient extends CosmosClient { + public readonly senderAddress: string; + + private readonly signCallback: SigningCallback; + private readonly fees: FeeTable; + + /** + * Creates a new client with signing capability to interact with a CosmWasm blockchain. This is the bigger brother of CosmWasmClient. + * + * This instance does a lot of caching. In order to benefit from that you should try to use one instance + * for the lifetime of your application. When switching backends, a new instance must be created. + * + * @param apiUrl The URL of a Cosmos SDK light client daemon API (sometimes called REST server or REST API) + * @param senderAddress The address that will sign and send transactions using this instance + * @param signCallback An asynchonous callback to create a signature for a given transaction. This can be implemented using secure key stores that require user interaction. + * @param customFees The fees that are paid for transactions + * @param broadcastMode Defines at which point of the transaction processing the postTx method (i.e. transaction broadcasting) returns + */ + public constructor( + apiUrl: string, + senderAddress: string, + signCallback: SigningCallback, + customFees?: Partial, + broadcastMode = BroadcastMode.Block, + ) { + super(apiUrl, broadcastMode); + this.anyValidAddress = senderAddress; + + this.senderAddress = senderAddress; + this.signCallback = signCallback; + this.fees = { ...defaultFees, ...(customFees || {}) }; + } + + public async getNonce(address?: string): Promise { + return super.getNonce(address || this.senderAddress); + } + + public async getAccount(address?: string): Promise { + return super.getAccount(address || this.senderAddress); + } + + public async sendTokens( + recipientAddress: string, + transferAmount: readonly Coin[], + memo = "", + ): Promise { + const sendMsg: MsgSend = { + type: "cosmos-sdk/MsgSend", + value: { + // eslint-disable-next-line @typescript-eslint/camelcase + from_address: this.senderAddress, + // eslint-disable-next-line @typescript-eslint/camelcase + to_address: recipientAddress, + amount: transferAmount, + }, + }; + const fee = this.fees.send; + const { accountNumber, sequence } = await this.getNonce(); + const chainId = await this.getChainId(); + const signBytes = makeSignBytes([sendMsg], fee, chainId, memo, accountNumber, sequence); + const signature = await this.signCallback(signBytes); + const signedTx = { + msg: [sendMsg], + fee: fee, + memo: memo, + signatures: [signature], + }; + + return this.postTx(signedTx); + } +} diff --git a/packages/sdk38/src/testdata/cosmoshub.json b/packages/sdk38/src/testdata/cosmoshub.json new file mode 100644 index 00000000..cb33539c --- /dev/null +++ b/packages/sdk38/src/testdata/cosmoshub.json @@ -0,0 +1,44 @@ +{ + "//source": "https://hubble.figment.network/cosmos/chains/cosmoshub-3/blocks/415777/transactions/2BD600EA6090FC75FD844CA73542CC90A828770F4C01C5B483C3C1C43CCB65F4?format=json", + "tx": { + "type": "cosmos-sdk/StdTx", + "value": { + "msg": [ + { + "type": "cosmos-sdk/MsgSend", + "value": { + "from_address": "cosmos1txqfn5jmcts0x0q7krdxj8tgf98tj0965vqlmq", + "to_address": "cosmos1nynns8ex9fq6sjjfj8k79ymkdz4sqth06xexae", + "amount": [ + { + "denom": "uatom", + "amount": "35997500" + } + ] + } + } + ], + "fee": { + "amount": [ + { + "denom": "uatom", + "amount": "2500" + } + ], + "gas": "100000" + }, + "signatures": [ + { + "pub_key": { + "type": "tendermint/PubKeySecp256k1", + "value": "A5qFcJBJvEK/fOmEAY0DHNWwSRZ9TEfNZyH8VoVvDtAq" + }, + "signature": "NK1Oy4EUGAsoC03c1wi9GG03JC/39LEdautC5Jk643oIbEPqeXHMwaqbdvO/Jws0X/NAXaN8SAy2KNY5Qml+5Q==" + } + ], + "memo": "" + } + }, + "tx_data": "ygEoKBapCkOoo2GaChRZgJnSW8Lg8zwesNppHWhJTrk8uhIUmSc4HyYqQahKSZHt4pN2aKsALu8aEQoFdWF0b20SCDM1OTk3NTAwEhMKDQoFdWF0b20SBDI1MDAQoI0GGmoKJuta6YchA5qFcJBJvEK/fOmEAY0DHNWwSRZ9TEfNZyH8VoVvDtAqEkA0rU7LgRQYCygLTdzXCL0YbTckL/f0sR1q60LkmTrjeghsQ+p5cczBqpt2878nCzRf80Bdo3xIDLYo1jlCaX7l", + "id": "2BD600EA6090FC75FD844CA73542CC90A828770F4C01C5B483C3C1C43CCB65F4" +} diff --git a/packages/sdk/src/testdata/txresponse1.json b/packages/sdk38/src/testdata/txresponse1.json similarity index 100% rename from packages/sdk/src/testdata/txresponse1.json rename to packages/sdk38/src/testdata/txresponse1.json diff --git a/packages/sdk/src/testdata/txresponse2.json b/packages/sdk38/src/testdata/txresponse2.json similarity index 100% rename from packages/sdk/src/testdata/txresponse2.json rename to packages/sdk38/src/testdata/txresponse2.json diff --git a/packages/sdk/src/testdata/txresponse3.json b/packages/sdk38/src/testdata/txresponse3.json similarity index 100% rename from packages/sdk/src/testdata/txresponse3.json rename to packages/sdk38/src/testdata/txresponse3.json diff --git a/packages/sdk38/src/testutils.spec.ts b/packages/sdk38/src/testutils.spec.ts new file mode 100644 index 00000000..7b8f2330 --- /dev/null +++ b/packages/sdk38/src/testutils.spec.ts @@ -0,0 +1,58 @@ +import { Random } from "@iov/crypto"; +import { Bech32 } from "@iov/encoding"; + +export function makeRandomAddress(): string { + return Bech32.encode("cosmos", Random.getBytes(20)); +} + +export const nonNegativeIntegerMatcher = /^[0-9]+$/; +export const tendermintIdMatcher = /^[0-9A-F]{64}$/; +export const tendermintOptionalIdMatcher = /^([0-9A-F]{64}|)$/; +export const tendermintAddressMatcher = /^[0-9A-F]{40}$/; +export const tendermintShortHashMatcher = /^[0-9a-f]{40}$/; +export const semverMatcher = /^[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z-]+(\.[0-9A-Za-z-]+)*)?$/; + +// https://github.com/bitcoin/bips/blob/master/bip-0173.mediawiki#bech32 +export const bech32AddressMatcher = /^[\x21-\x7e]{1,83}1[02-9ac-hj-np-z]{38}$/; + +export const wasmd = { + endpoint: "http://localhost:1317", + chainId: "testing", +}; + +export const faucet = { + mnemonic: + "economy stock theory fatal elder harbor betray wasp final emotion task crumble siren bottom lizard educate guess current outdoor pair theory focus wife stone", + pubkey: { + type: "tendermint/PubKeySecp256k1", + value: "A08EGB7ro1ORuFhjOnZcSgwYlpe0DSFjVNUIkNNQxwKQ", + }, + address: "cosmos1pkptre7fdkl6gfrzlesjjvhxhlc3r4gmmk8rs6", +}; + +/** Unused account */ +export const unused = { + pubkey: { + type: "tendermint/PubKeySecp256k1", + value: "ArkCaFUJ/IH+vKBmNRCdUVl3mCAhbopk9jjW4Ko4OfRQ", + }, + address: "cosmos1cjsxept9rkggzxztslae9ndgpdyt2408lk850u", + accountNumber: 9, + sequence: 0, +}; + +export function wasmdEnabled(): boolean { + return !!process.env.WASMD_ENABLED; +} + +export function pendingWithoutWasmd(): void { + if (!wasmdEnabled()) { + return pending("Set WASMD_ENABLED to enable Wasmd based tests"); + } +} + +/** Returns first element. Throws if array has a different length than 1. */ +export function fromOneElementArray(elements: ArrayLike): T { + if (elements.length !== 1) throw new Error(`Expected exactly one element but got ${elements.length}`); + return elements[0]; +} diff --git a/packages/sdk38/src/types.ts b/packages/sdk38/src/types.ts new file mode 100644 index 00000000..476cb555 --- /dev/null +++ b/packages/sdk38/src/types.ts @@ -0,0 +1,73 @@ +import { Coin } from "./coins"; + +/** An Amino/Cosmos SDK StdTx */ +export interface StdTx { + readonly msg: ReadonlyArray; + readonly fee: StdFee; + readonly signatures: ReadonlyArray; + readonly memo: string | undefined; +} + +export function isStdTx(txValue: unknown): txValue is StdTx { + const { memo, msg, fee, signatures } = txValue as StdTx; + return ( + typeof memo === "string" && Array.isArray(msg) && typeof fee === "object" && Array.isArray(signatures) + ); +} + +export interface CosmosSdkTx { + readonly type: string; + readonly value: StdTx; +} + +export interface Msg { + readonly type: string; + readonly value: any; +} + +/** A Cosmos SDK token transfer message */ +export interface MsgSend extends Msg { + readonly type: "cosmos-sdk/MsgSend"; + readonly value: { + /** Bech32 account address */ + readonly from_address: string; + /** Bech32 account address */ + readonly to_address: string; + readonly amount: ReadonlyArray; + }; +} + +export function isMsgSend(msg: Msg): msg is MsgSend { + return (msg as MsgSend).type === "cosmos-sdk/MsgSend"; +} + +export interface StdFee { + readonly amount: ReadonlyArray; + readonly gas: string; +} + +export interface StdSignature { + readonly pub_key: PubKey; + readonly signature: string; +} + +export interface PubKey { + // type is one of the strings defined in pubkeyTypes + // I don't use a string literal union here as that makes trouble with json test data: + // https://github.com/confio/cosmwasm-js/pull/44#pullrequestreview-353280504 + readonly type: string; + // Value field is base64-encoded in all cases + // Note: if type is Secp256k1, this must contain a COMPRESSED pubkey - to encode from bcp/keycontrol land, you must compress it first + readonly value: string; +} + +export const pubkeyType = { + /** @see https://github.com/tendermint/tendermint/blob/v0.33.0/crypto/ed25519/ed25519.go#L22 */ + secp256k1: "tendermint/PubKeySecp256k1" as const, + /** @see https://github.com/tendermint/tendermint/blob/v0.33.0/crypto/secp256k1/secp256k1.go#L23 */ + ed25519: "tendermint/PubKeyEd25519" as const, + /** @see https://github.com/tendermint/tendermint/blob/v0.33.0/crypto/sr25519/codec.go#L12 */ + sr25519: "tendermint/PubKeySr25519" as const, +}; + +export const pubkeyTypes: readonly string[] = [pubkeyType.secp256k1, pubkeyType.ed25519, pubkeyType.sr25519]; diff --git a/packages/sdk38/tsconfig.json b/packages/sdk38/tsconfig.json new file mode 100644 index 00000000..167e8c02 --- /dev/null +++ b/packages/sdk38/tsconfig.json @@ -0,0 +1,12 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "baseUrl": ".", + "outDir": "build", + "declarationDir": "build/types", + "rootDir": "src" + }, + "include": [ + "src/**/*" + ] +} diff --git a/packages/sdk38/typedoc.js b/packages/sdk38/typedoc.js new file mode 100644 index 00000000..e2387c7d --- /dev/null +++ b/packages/sdk38/typedoc.js @@ -0,0 +1,14 @@ +const packageJson = require("./package.json"); + +module.exports = { + src: ["./src"], + out: "docs", + exclude: "**/*.spec.ts", + target: "es6", + name: `${packageJson.name} Documentation`, + readme: "README.md", + mode: "file", + excludeExternals: true, + excludeNotExported: true, + excludePrivate: true, +}; diff --git a/packages/sdk/types/address.d.ts b/packages/sdk38/types/address.d.ts similarity index 100% rename from packages/sdk/types/address.d.ts rename to packages/sdk38/types/address.d.ts diff --git a/packages/sdk/types/coins.d.ts b/packages/sdk38/types/coins.d.ts similarity index 100% rename from packages/sdk/types/coins.d.ts rename to packages/sdk38/types/coins.d.ts diff --git a/packages/sdk38/types/cosmosclient.d.ts b/packages/sdk38/types/cosmosclient.d.ts new file mode 100644 index 00000000..0af6120a --- /dev/null +++ b/packages/sdk38/types/cosmosclient.d.ts @@ -0,0 +1,128 @@ +import { Coin } from "./coins"; +import { Log } from "./logs"; +import { BroadcastMode, RestClient } from "./restclient"; +import { CosmosSdkTx, PubKey, StdTx } from "./types"; +export interface GetNonceResult { + readonly accountNumber: number; + readonly sequence: number; +} +export interface Account { + /** Bech32 account address */ + readonly address: string; + readonly balance: ReadonlyArray; + readonly pubkey: PubKey | undefined; + readonly accountNumber: number; + readonly sequence: number; +} +export interface PostTxResult { + readonly logs: readonly Log[]; + readonly rawLog: string; + /** Transaction hash (might be used as transaction ID). Guaranteed to be non-empty upper-case hex */ + readonly transactionHash: string; +} +export interface SearchByIdQuery { + readonly id: string; +} +export interface SearchByHeightQuery { + readonly height: number; +} +export interface SearchBySentFromOrToQuery { + readonly sentFromOrTo: string; +} +/** + * This query type allows you to pass arbitrary key/value pairs to the backend. It is + * more powerful and slightly lower level than the other search options. + */ +export interface SearchByTagsQuery { + readonly tags: readonly { + readonly key: string; + readonly value: string; + }[]; +} +export declare type SearchTxQuery = + | SearchByIdQuery + | SearchByHeightQuery + | SearchBySentFromOrToQuery + | SearchByTagsQuery; +export interface SearchTxFilter { + readonly minHeight?: number; + readonly maxHeight?: number; +} +/** A transaction that is indexed as part of the transaction history */ +export interface IndexedTx { + readonly height: number; + /** Transaction hash (might be used as transaction ID). Guaranteed to be non-empty upper-case hex */ + readonly hash: string; + /** Transaction execution error code. 0 on success. */ + readonly code: number; + readonly rawLog: string; + readonly logs: readonly Log[]; + readonly tx: CosmosSdkTx; + /** The gas limit as set by the user */ + readonly gasWanted?: number; + /** The gas used by the execution */ + readonly gasUsed?: number; + /** An RFC 3339 time string like e.g. '2020-02-15T10:39:10.4696305Z' */ + readonly timestamp: string; +} +export interface BlockHeader { + readonly version: { + readonly block: string; + readonly app: string; + }; + readonly height: number; + readonly chainId: string; + /** An RFC 3339 time string like e.g. '2020-02-15T10:39:10.4696305Z' */ + readonly time: string; +} +export interface Block { + /** The ID is a hash of the block header (uppercase hex) */ + readonly id: string; + readonly header: BlockHeader; + /** Array of raw transactions */ + readonly txs: ReadonlyArray; +} +/** Use for testing only */ +export interface PrivateCosmWasmClient { + readonly restClient: RestClient; +} +export declare class CosmosClient { + protected readonly restClient: RestClient; + /** Any address the chain considers valid (valid bech32 with proper prefix) */ + protected anyValidAddress: string | undefined; + private chainId; + /** + * Creates a new client to interact with a CosmWasm blockchain. + * + * This instance does a lot of caching. In order to benefit from that you should try to use one instance + * for the lifetime of your application. When switching backends, a new instance must be created. + * + * @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); + getChainId(): Promise; + getHeight(): Promise; + /** + * Returns a 32 byte upper-case hex transaction hash (typically used as the transaction ID) + */ + getIdentifier(tx: CosmosSdkTx): Promise; + /** + * Returns account number and sequence. + * + * Throws if the account does not exist on chain. + * + * @param address returns data for this address. When unset, the client's sender adddress is used. + */ + getNonce(address: string): Promise; + getAccount(address: string): Promise; + /** + * Gets block header and meta + * + * @param height The height of the block. If undefined, the latest height is used. + */ + getBlock(height?: number): Promise; + searchTx(query: SearchTxQuery, filter?: SearchTxFilter): Promise; + postTx(tx: StdTx): Promise; + private txsQuery; +} diff --git a/packages/sdk/types/decoding.d.ts b/packages/sdk38/types/decoding.d.ts similarity index 100% rename from packages/sdk/types/decoding.d.ts rename to packages/sdk38/types/decoding.d.ts diff --git a/packages/sdk/types/encoding.d.ts b/packages/sdk38/types/encoding.d.ts similarity index 100% rename from packages/sdk/types/encoding.d.ts rename to packages/sdk38/types/encoding.d.ts diff --git a/packages/sdk/types/index.d.ts b/packages/sdk38/types/index.d.ts similarity index 50% rename from packages/sdk/types/index.d.ts rename to packages/sdk38/types/index.d.ts index 474b2dd3..2823e7e2 100644 --- a/packages/sdk/types/index.d.ts +++ b/packages/sdk38/types/index.d.ts @@ -1,20 +1,12 @@ import * as logs from "./logs"; -import * as types from "./types"; -export { logs, types }; -export { pubkeyToAddress } from "./address"; +export { logs }; +export { pubkeyToAddress, rawSecp256k1PubkeyToAddress } from "./address"; export { Coin, coin, coins } from "./coins"; -export { unmarshalTx } from "./decoding"; -export { makeSignBytes, marshalTx } from "./encoding"; -export { BroadcastMode, RestClient, TxsResponse } from "./restclient"; export { Account, Block, BlockHeader, - Code, - CodeDetails, - Contract, - ContractDetails, - CosmWasmClient, + CosmosClient, GetNonceResult, IndexedTx, PostTxResult, @@ -24,17 +16,33 @@ export { SearchByTagsQuery, SearchTxQuery, SearchTxFilter, -} from "./cosmwasmclient"; -export { makeCosmoshubPath, Pen, PrehashType, Secp256k1Pen } from "./pen"; +} from "./cosmosclient"; +export { unmarshalTx } from "./decoding"; +export { makeSignBytes, marshalTx } from "./encoding"; +export { + AuthAccountsResponse, + BlockResponse, + BroadcastMode, + PostTxsResponse, + NodeInfoResponse, + RestClient, + SearchTxsResponse, + TxsResponse, +} from "./restclient"; +export { Pen, Secp256k1Pen, makeCosmoshubPath } from "./pen"; export { decodeBech32Pubkey, encodeBech32Pubkey, encodeSecp256k1Pubkey } from "./pubkey"; export { findSequenceForSignedTx } from "./sequence"; export { encodeSecp256k1Signature, decodeSignature } from "./signature"; +export { FeeTable, SigningCallback, SigningCosmosClient } from "./signingcosmosclient"; export { - ExecuteResult, - FeeTable, - InstantiateResult, - SigningCallback, - SigningCosmWasmClient, - UploadMeta, - UploadResult, -} from "./signingcosmwasmclient"; + isMsgSend, + isStdTx, + pubkeyType, + CosmosSdkTx, + PubKey, + Msg, + MsgSend, + StdFee, + StdSignature, + StdTx, +} from "./types"; diff --git a/packages/sdk38/types/logs.d.ts b/packages/sdk38/types/logs.d.ts new file mode 100644 index 00000000..2e1decb3 --- /dev/null +++ b/packages/sdk38/types/logs.d.ts @@ -0,0 +1,28 @@ +export interface Attribute { + readonly key: string; + readonly value: string; +} +export interface Event { + readonly type: string; + readonly attributes: readonly Attribute[]; +} +export interface Log { + readonly msg_index: number; + readonly log: string; + readonly events: readonly Event[]; +} +export declare function parseAttribute(input: unknown): Attribute; +export declare function parseEvent(input: unknown): Event; +export declare function parseLog(input: unknown): Log; +export declare function parseLogs(input: unknown): readonly Log[]; +/** + * Searches in logs for the first event of the given event type and in that event + * for the first first attribute with the given attribute key. + * + * Throws if the attribute was not found. + */ +export declare function findAttribute( + logs: readonly Log[], + eventType: "message" | "transfer", + attrKey: string, +): Attribute; diff --git a/packages/sdk/types/pen.d.ts b/packages/sdk38/types/pen.d.ts similarity index 100% rename from packages/sdk/types/pen.d.ts rename to packages/sdk38/types/pen.d.ts diff --git a/packages/sdk/types/pubkey.d.ts b/packages/sdk38/types/pubkey.d.ts similarity index 100% rename from packages/sdk/types/pubkey.d.ts rename to packages/sdk38/types/pubkey.d.ts diff --git a/packages/sdk/types/restclient.d.ts b/packages/sdk38/types/restclient.d.ts similarity index 67% rename from packages/sdk/types/restclient.d.ts rename to packages/sdk38/types/restclient.d.ts index 5d5deea4..0ecbfe88 100644 --- a/packages/sdk/types/restclient.d.ts +++ b/packages/sdk38/types/restclient.d.ts @@ -1,5 +1,5 @@ import { Coin } from "./coins"; -import { CosmosSdkTx, JsonObject, Model, StdTx } from "./types"; +import { CosmosSdkTx, StdTx } from "./types"; export interface CosmosSdkAccount { /** Bech32 account address */ readonly address: string; @@ -9,7 +9,7 @@ export interface CosmosSdkAccount { readonly account_number: number; readonly sequence: number; } -export interface NodeInfo { +interface NodeInfo { readonly protocol_version: { readonly p2p: string; readonly block: string; @@ -26,7 +26,7 @@ export interface NodeInfo { readonly rpc_address: string; }; } -export interface ApplicationVersion { +interface ApplicationVersion { readonly name: string; readonly server_name: string; readonly client_name: string; @@ -39,10 +39,10 @@ export interface NodeInfoResponse { readonly node_info: NodeInfo; readonly application_version: ApplicationVersion; } -export interface BlockId { +interface BlockId { readonly hash: string; } -export interface BlockHeader { +interface BlockHeader { readonly version: { readonly block: string; readonly app: string; @@ -65,7 +65,7 @@ export interface BlockHeader { readonly evidence_hash: string; readonly proposer_address: string; } -export interface Block { +interface Block { readonly header: BlockHeader; readonly data: { /** Array of base64 encoded transactions */ @@ -76,21 +76,13 @@ export interface BlockResponse { readonly block_id: BlockId; readonly block: Block; } -interface AuthAccountsResponse { +export interface AuthAccountsResponse { readonly height: string; readonly result: { readonly type: "cosmos-sdk/Account"; readonly value: CosmosSdkAccount; }; } -declare type WasmResponse = WasmSuccess | WasmError; -interface WasmSuccess { - readonly height: string; - readonly result: T; -} -interface WasmError { - readonly error: string; -} export interface TxsResponse { readonly height: string; readonly txhash: string; @@ -107,7 +99,7 @@ export interface TxsResponse { readonly gas_used?: string; readonly timestamp: string; } -interface SearchTxsResponse { +export interface SearchTxsResponse { readonly total_count: string; readonly count: string; readonly page_number: string; @@ -127,46 +119,6 @@ export interface PostTxsResponse { /** The gas used by the execution */ readonly gas_used?: string; } -interface EncodeTxResponse { - readonly tx: 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; - 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; - readonly label: string; -} -export interface ContractDetails extends ContractInfo { - /** Argument passed on initialization of the contract */ - readonly init_msg: object; -} -declare type RestClientResponse = - | NodeInfoResponse - | BlockResponse - | AuthAccountsResponse - | TxsResponse - | SearchTxsResponse - | PostTxsResponse - | EncodeTxResponse - | WasmResponse - | WasmResponse - | WasmResponse - | WasmResponse - | WasmResponse; /** * The mode used to send transaction * @@ -195,8 +147,8 @@ export declare class RestClient { * @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; + get(path: string): Promise; + post(path: string, params: any): Promise; authAccounts(address: string): Promise; blocksLatest(): Promise; blocks(height: number): Promise; @@ -213,19 +165,5 @@ export declare class RestClient { * @param tx a signed transaction as StdTx (i.e. not wrapped in type/value container) */ postTx(tx: StdTx): Promise; - 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; } export {}; diff --git a/packages/sdk/types/sequence.d.ts b/packages/sdk38/types/sequence.d.ts similarity index 100% rename from packages/sdk/types/sequence.d.ts rename to packages/sdk38/types/sequence.d.ts diff --git a/packages/sdk/types/signature.d.ts b/packages/sdk38/types/signature.d.ts similarity index 100% rename from packages/sdk/types/signature.d.ts rename to packages/sdk38/types/signature.d.ts diff --git a/packages/sdk38/types/signingcosmosclient.d.ts b/packages/sdk38/types/signingcosmosclient.d.ts new file mode 100644 index 00000000..d9ac523a --- /dev/null +++ b/packages/sdk38/types/signingcosmosclient.d.ts @@ -0,0 +1,40 @@ +import { Coin } from "./coins"; +import { Account, CosmosClient, GetNonceResult, PostTxResult } from "./cosmosclient"; +import { BroadcastMode } from "./restclient"; +import { StdFee, StdSignature } from "./types"; +export interface SigningCallback { + (signBytes: Uint8Array): Promise; +} +export interface FeeTable { + readonly upload: StdFee; + readonly init: StdFee; + readonly exec: StdFee; + readonly send: StdFee; +} +export declare class SigningCosmosClient extends CosmosClient { + readonly senderAddress: string; + private readonly signCallback; + private readonly fees; + /** + * Creates a new client with signing capability to interact with a CosmWasm blockchain. This is the bigger brother of CosmWasmClient. + * + * This instance does a lot of caching. In order to benefit from that you should try to use one instance + * for the lifetime of your application. When switching backends, a new instance must be created. + * + * @param apiUrl The URL of a Cosmos SDK light client daemon API (sometimes called REST server or REST API) + * @param senderAddress The address that will sign and send transactions using this instance + * @param signCallback An asynchonous callback to create a signature for a given transaction. This can be implemented using secure key stores that require user interaction. + * @param customFees The fees that are paid for transactions + * @param broadcastMode Defines at which point of the transaction processing the postTx method (i.e. transaction broadcasting) returns + */ + constructor( + apiUrl: string, + senderAddress: string, + signCallback: SigningCallback, + customFees?: Partial, + broadcastMode?: BroadcastMode, + ); + getNonce(address?: string): Promise; + getAccount(address?: string): Promise; + sendTokens(recipientAddress: string, transferAmount: readonly Coin[], memo?: string): Promise; +} diff --git a/packages/sdk38/types/types.d.ts b/packages/sdk38/types/types.d.ts new file mode 100644 index 00000000..bb8390ed --- /dev/null +++ b/packages/sdk38/types/types.d.ts @@ -0,0 +1,50 @@ +import { Coin } from "./coins"; +/** An Amino/Cosmos SDK StdTx */ +export interface StdTx { + readonly msg: ReadonlyArray; + readonly fee: StdFee; + readonly signatures: ReadonlyArray; + readonly memo: string | undefined; +} +export declare function isStdTx(txValue: unknown): txValue is StdTx; +export interface CosmosSdkTx { + readonly type: string; + readonly value: StdTx; +} +export interface Msg { + readonly type: string; + readonly value: any; +} +/** A Cosmos SDK token transfer message */ +export interface MsgSend extends Msg { + readonly type: "cosmos-sdk/MsgSend"; + readonly value: { + /** Bech32 account address */ + readonly from_address: string; + /** Bech32 account address */ + readonly to_address: string; + readonly amount: ReadonlyArray; + }; +} +export declare function isMsgSend(msg: Msg): msg is MsgSend; +export interface StdFee { + readonly amount: ReadonlyArray; + readonly gas: string; +} +export interface StdSignature { + readonly pub_key: PubKey; + readonly signature: string; +} +export interface PubKey { + readonly type: string; + readonly value: string; +} +export declare const pubkeyType: { + /** @see https://github.com/tendermint/tendermint/blob/v0.33.0/crypto/ed25519/ed25519.go#L22 */ + secp256k1: "tendermint/PubKeySecp256k1"; + /** @see https://github.com/tendermint/tendermint/blob/v0.33.0/crypto/secp256k1/secp256k1.go#L23 */ + ed25519: "tendermint/PubKeyEd25519"; + /** @see https://github.com/tendermint/tendermint/blob/v0.33.0/crypto/sr25519/codec.go#L12 */ + sr25519: "tendermint/PubKeySr25519"; +}; +export declare const pubkeyTypes: readonly string[]; diff --git a/packages/sdk38/webpack.web.config.js b/packages/sdk38/webpack.web.config.js new file mode 100644 index 00000000..7373cace --- /dev/null +++ b/packages/sdk38/webpack.web.config.js @@ -0,0 +1,19 @@ +const glob = require("glob"); +const path = require("path"); +const webpack = require("webpack"); + +const target = "web"; +const distdir = path.join(__dirname, "dist", "web"); + +module.exports = [ + { + // bundle used for Karma tests + target: target, + entry: glob.sync("./build/**/*.spec.js"), + output: { + path: distdir, + filename: "tests.js", + }, + plugins: [new webpack.EnvironmentPlugin(["WASMD_ENABLED"])], + }, +]; diff --git a/scripts/wasmd/deploy_erc20.js b/scripts/wasmd/deploy_erc20.js index 95b2db50..8fc612a9 100755 --- a/scripts/wasmd/deploy_erc20.js +++ b/scripts/wasmd/deploy_erc20.js @@ -1,7 +1,8 @@ #!/usr/bin/env node /* eslint-disable @typescript-eslint/camelcase */ -const { SigningCosmWasmClient, Secp256k1Pen } = require("@cosmwasm/sdk"); +const { SigningCosmWasmClient } = require("@cosmwasm/cosmwasm"); +const { Secp256k1Pen } = require("@cosmwasm/sdk38"); const fs = require("fs"); const httpUrl = "http://localhost:1317"; diff --git a/scripts/wasmd/deploy_nameservice.js b/scripts/wasmd/deploy_nameservice.js index ca9de2ef..d2e3c196 100755 --- a/scripts/wasmd/deploy_nameservice.js +++ b/scripts/wasmd/deploy_nameservice.js @@ -1,7 +1,8 @@ #!/usr/bin/env node /* eslint-disable @typescript-eslint/camelcase */ -const { SigningCosmWasmClient, Secp256k1Pen } = require("@cosmwasm/sdk"); +const { SigningCosmWasmClient } = require("@cosmwasm/cosmwasm"); +const { Secp256k1Pen } = require("@cosmwasm/sdk38"); const fs = require("fs"); const httpUrl = "http://localhost:1317"; diff --git a/scripts/wasmd/deploy_staking.js b/scripts/wasmd/deploy_staking.js index 1369fddf..56341cbc 100755 --- a/scripts/wasmd/deploy_staking.js +++ b/scripts/wasmd/deploy_staking.js @@ -1,7 +1,8 @@ #!/usr/bin/env node /* eslint-disable @typescript-eslint/camelcase */ -const { SigningCosmWasmClient, Secp256k1Pen } = require("@cosmwasm/sdk"); +const { SigningCosmWasmClient } = require("@cosmwasm/cosmwasm"); +const { Secp256k1Pen } = require("@cosmwasm/sdk38"); const fs = require("fs"); const httpUrl = "http://localhost:1317";