Merge pull request #183 from CosmWasm/split-cosmossdk-cosmwasm

Split Cosmos SDK and CosmWasm functionality
This commit is contained in:
Simon Warta 2020-06-03 13:30:23 +02:00 committed by GitHub
commit 4a350c2409
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
131 changed files with 3611 additions and 729 deletions

View File

@ -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",

View File

@ -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 {

View File

@ -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,

View File

@ -1,4 +1,4 @@
import { decodeSignature } from "@cosmwasm/sdk";
import { decodeSignature } from "@cosmwasm/sdk38";
import {
Account,
Address,

View File

@ -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<PostTxResponse> {
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 {

View File

@ -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: {

View File

@ -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[],

View File

@ -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,

View File

@ -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[],

View File

@ -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;

View File

@ -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,

View File

@ -1,6 +1,6 @@
const defaultHttpUrl = "http://localhost:1317";
const defaultNetworkId = "testing";
const defaultFee: types.StdFee = {
const defaultFee: StdFee = {
amount: [
{
amount: "5000",

View File

@ -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,

View File

@ -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",

View File

@ -30,19 +30,8 @@ export function main(originalArgs: readonly string[]): void {
const imports = new Map<string, readonly string[]>([
[
"@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",
[

View File

@ -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.

View File

@ -1,5 +1,5 @@
{
"name": "@cosmwasm/sdk",
"name": "@cosmwasm/cosmwasm",
"version": "0.8.0",
"description": "CosmWasm SDK",
"author": "Ethan Frey <ethanfrey@users.noreply.github.com>",
@ -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",

View File

@ -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:

View File

@ -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;

View File

@ -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;

View File

@ -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";

View File

@ -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<Coin>;
};
}
/**
* 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<Coin>;
};
}
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";
}

View File

@ -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;

View File

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

View File

@ -1,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";

View File

@ -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<StdSignature>;

View File

@ -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;

View File

@ -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;

38
packages/cosmwasm/types/index.d.ts vendored Normal file
View File

@ -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";

58
packages/cosmwasm/types/msgs.d.ts vendored Normal file
View File

@ -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<Coin>;
};
}
/**
* 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<Coin>;
};
}
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;

70
packages/cosmwasm/types/restclient.d.ts vendored Normal file
View File

@ -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<readonly CodeInfo[]>;
getCode(id: number): Promise<CodeDetails>;
listContractsByCodeId(id: number): Promise<readonly ContractInfo[]>;
/**
* Returns null when contract was not found at this address.
*/
getContractInfo(address: string): Promise<ContractDetails | null>;
getAllContractState(address: string): Promise<readonly Model[]>;
queryContractRaw(address: string, key: Uint8Array): Promise<Uint8Array | null>;
/**
* Makes a smart query on the contract and parses the reponse as JSON.
* Throws error if no such contract exists, the query format is invalid or the response is invalid.
*/
queryContractSmart(address: string, query: object): Promise<JsonObject>;
}

View File

@ -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<StdSignature>;
}

14
packages/cosmwasm/types/types.d.ts vendored Normal file
View File

@ -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;

View File

@ -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",

View File

@ -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,

View File

@ -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)

View File

@ -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<Msg>;
readonly fee: StdFee;
readonly signatures: ReadonlyArray<StdSignature>;
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<Coin>;
};
}
/**
* 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<Coin>;
};
}
/**
* 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<Coin>;
};
}
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<Coin>;
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;

View File

@ -1,123 +0,0 @@
import { Coin } from "./coins";
/** An Amino/Cosmos SDK StdTx */
export interface StdTx {
readonly msg: ReadonlyArray<Msg>;
readonly fee: StdFee;
readonly signatures: ReadonlyArray<StdSignature>;
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<Coin>;
};
}
/**
* 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<Coin>;
};
}
/**
* 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<Coin>;
};
}
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<Coin>;
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 {};

View File

@ -0,0 +1 @@
../../.eslintignore

3
packages/sdk38/.gitignore vendored Normal file
View File

@ -0,0 +1,3 @@
build/
dist/
docs/

12
packages/sdk38/README.md Normal file
View File

@ -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)).

View File

@ -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();

View File

@ -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"],
},
},
});
};

View File

@ -0,0 +1 @@
Directory used to trigger lerna package updates for all packages

View File

@ -0,0 +1,48 @@
{
"name": "@cosmwasm/sdk38",
"version": "0.8.0",
"description": "Utilities for Cosmos SDK 0.38",
"author": "Ethan Frey <ethanfrey@users.noreply.github.com>",
"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"
}
}

View File

@ -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,
}),
);
});
});
});

View File

@ -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}$/);
});
});
});

View File

@ -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<Coin>;
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<Uint8Array>;
}
/** 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<string> {
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<number> {
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<string> {
// 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<GetNonceResult> {
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<Account | undefined> {
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<Block> {
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<readonly IndexedTx[]> {
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<PostTxResult> {
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<readonly IndexedTx[]> {
// 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,
}),
);
}
}

View File

@ -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";

View File

@ -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);
});
});
});

View File

@ -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;
}

View File

@ -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);
});
});
});

View File

@ -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<string>
| WasmResponse<CodeInfo[]>
| WasmResponse<CodeDetails>
| WasmResponse<ContractInfo[] | null>
| WasmResponse<ContractDetails | null>;
/** Unfortunately, Cosmos SDK encodes empty arrays as null */
type CosmosSdkArray<T> = ReadonlyArray<T> | null;
function normalizeArray<T>(backend: CosmosSdkArray<T>): ReadonlyArray<T> {
return backend || [];
}
/**
* The mode used to send transaction
*
@ -231,17 +173,6 @@ export enum BroadcastMode {
Async = "async",
}
function isWasmError<T>(resp: WasmResponse<T>): resp is WasmError {
return (resp as WasmError).error !== undefined;
}
function unwrapWasmResponse<T>(response: WasmResponse<T>): T {
if (isWasmError(response)) {
throw new Error(response.error);
}
return response.result;
}
// 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<RestClientResponse> {
public async get(path: string): Promise<any> {
const { data } = await this.client.get(path).catch(parseAxiosError);
if (data === null) {
throw new Error("Received null response from server");
@ -298,7 +229,7 @@ export class RestClient {
return data;
}
public async post(path: string, params: any): Promise<RestClientResponse> {
public async post(path: string, params: any): Promise<any> {
if (!isNonNullObject(params)) throw new Error("Got unexpected type of params. Expected object.");
const { data } = await this.client.post(path, params).catch(parseAxiosError);
if (data === null) {
@ -312,7 +243,7 @@ export class RestClient {
public async authAccounts(address: string): Promise<AuthAccountsResponse> {
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<BlockResponse> {
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<BlockResponse> {
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<NodeInfoResponse> {
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<TxsResponse> {
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<SearchTxsResponse> {
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<Uint8Array> {
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<readonly CodeInfo[]> {
const path = `/wasm/code`;
const responseData = (await this.get(path)) as WasmResponse<CosmosSdkArray<CodeInfo>>;
return normalizeArray(unwrapWasmResponse(responseData));
}
// this will download the original wasm bytecode by code id
// throws error if no code with this id
public async getCode(id: number): Promise<CodeDetails> {
const path = `/wasm/code/${id}`;
const responseData = (await this.get(path)) as WasmResponse<CodeDetails>;
return unwrapWasmResponse(responseData);
}
public async listContractsByCodeId(id: number): Promise<readonly ContractInfo[]> {
const path = `/wasm/code/${id}/contracts`;
const responseData = (await this.get(path)) as WasmResponse<CosmosSdkArray<ContractInfo>>;
return normalizeArray(unwrapWasmResponse(responseData));
}
/**
* Returns null when contract was not found at this address.
*/
public async getContractInfo(address: string): Promise<ContractDetails | null> {
const path = `/wasm/contract/${address}`;
const response = (await this.get(path)) as WasmResponse<ContractDetails | null>;
return unwrapWasmResponse(response);
}
// Returns all contract state.
// This is an empty array if no such contract, or contract has no data.
public async getAllContractState(address: string): Promise<readonly Model[]> {
const path = `/wasm/contract/${address}/state`;
const responseData = (await this.get(path)) as WasmResponse<CosmosSdkArray<WasmData>>;
return normalizeArray(unwrapWasmResponse(responseData)).map(parseWasmData);
}
// Returns the data at the key if present (unknown decoded json),
// or null if no data at this (contract address, key) pair
public async queryContractRaw(address: string, key: Uint8Array): Promise<Uint8Array | null> {
const hexKey = toHex(key);
const path = `/wasm/contract/${address}/raw/${hexKey}?encoding=hex`;
const responseData = (await this.get(path)) as WasmResponse<WasmData[]>;
const data = unwrapWasmResponse(responseData);
return data.length === 0 ? null : fromBase64(data[0].val);
}
/**
* Makes a smart query on the contract and parses the reponse as JSON.
* Throws error if no such contract exists, the query format is invalid or the response is invalid.
*/
public async queryContractSmart(address: string, query: object): Promise<JsonObject> {
const encoded = toHex(toUtf8(JSON.stringify(query)));
const path = `/wasm/contract/${address}/smart/${encoded}?encoding=hex`;
const responseData = (await this.get(path)) as WasmResponse<SmartQueryResponse>;
const result = unwrapWasmResponse(responseData);
// By convention, smart queries must return a valid JSON document (see https://github.com/CosmWasm/cosmwasm/issues/144)
return JSON.parse(fromUtf8(fromBase64(result.smart)));
}
}

Some files were not shown because too many files have changed in this diff Show More