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
131 changed files with 3611 additions and 729 deletions
+2 -1
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",
+4 -4
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 {
+1 -1
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,
+1 -1
View File
@@ -1,4 +1,4 @@
import { decodeSignature } from "@cosmwasm/sdk";
import { decodeSignature } from "@cosmwasm/sdk38";
import {
Account,
Address,
+12 -6
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 {
+10 -9
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: {
+24 -12
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[],
+16 -7
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,
+7 -7
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[],
+6 -6
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;
+2 -2
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,
+1 -1
View File
@@ -1,6 +1,6 @@
const defaultHttpUrl = "http://localhost:1317";
const defaultNetworkId = "testing";
const defaultFee: types.StdFee = {
const defaultFee: StdFee = {
amount: [
{
amount: "5000",
+4 -4
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,
+2 -1
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",
+25 -13
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",
[
@@ -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.
@@ -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",
@@ -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:
@@ -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;
@@ -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;
+39
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";
+70
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";
}
@@ -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;
+170
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)));
}
}
@@ -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";
@@ -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>;
+29
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;
@@ -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
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
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
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>;
}
@@ -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
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;
+2 -1
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",
+2 -1
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,
+1 -1
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
View File
View File
View File
View File
View File
View File
View File
View File
View File
View File
View File
View File
View File
View File
View File
View File
View File
View File
-174
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;
-123
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 {};
+1
View File
@@ -0,0 +1 @@
../../.eslintignore
+3
View File
@@ -0,0 +1,3 @@
build/
dist/
docs/
+12
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)).
+26
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();
+54
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"],
},
},
});
};
+1
View File
@@ -0,0 +1 @@
Directory used to trigger lerna package updates for all packages
+48
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"
}
}
@@ -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,
}),
);
});
});
});
+235
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}$/);
});
});
});
+314
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,
}),
);
}
}
@@ -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";
+165
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);
});
});
});
+86
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;
}
+897
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);
});
});
});
@@ -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