Merge pull request #183 from CosmWasm/split-cosmossdk-cosmwasm
Split Cosmos SDK and CosmWasm functionality
This commit is contained in:
commit
4a350c2409
@ -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",
|
||||
|
||||
@ -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,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,4 +1,4 @@
|
||||
import { decodeSignature } from "@cosmwasm/sdk";
|
||||
import { decodeSignature } from "@cosmwasm/sdk38";
|
||||
import {
|
||||
Account,
|
||||
Address,
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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: {
|
||||
|
||||
@ -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[],
|
||||
|
||||
@ -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,
|
||||
|
||||
14
packages/bcp/types/decode.d.ts
vendored
14
packages/bcp/types/decode.d.ts
vendored
@ -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[],
|
||||
|
||||
12
packages/bcp/types/encode.d.ts
vendored
12
packages/bcp/types/encode.d.ts
vendored
@ -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;
|
||||
|
||||
@ -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,6 +1,6 @@
|
||||
const defaultHttpUrl = "http://localhost:1317";
|
||||
const defaultNetworkId = "testing";
|
||||
const defaultFee: types.StdFee = {
|
||||
const defaultFee: StdFee = {
|
||||
amount: [
|
||||
{
|
||||
amount: "5000",
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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
|
||||
|
||||
[](https://www.npmjs.com/package/@cosmwasm/sdk)
|
||||
[](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
packages/cosmwasm/src/index.ts
Normal file
39
packages/cosmwasm/src/index.ts
Normal 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
packages/cosmwasm/src/msgs.ts
Normal file
70
packages/cosmwasm/src/msgs.ts
Normal 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
packages/cosmwasm/src/restclient.ts
Normal file
170
packages/cosmwasm/src/restclient.ts
Normal 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
packages/cosmwasm/src/types.ts
Normal file
29
packages/cosmwasm/src/types.ts
Normal 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
packages/cosmwasm/types/index.d.ts
vendored
Normal file
38
packages/cosmwasm/types/index.d.ts
vendored
Normal file
@ -0,0 +1,38 @@
|
||||
import * as logs from "./logs";
|
||||
export { logs };
|
||||
export { RestClient, TxsResponse } from "./restclient";
|
||||
export {
|
||||
Account,
|
||||
Block,
|
||||
BlockHeader,
|
||||
Code,
|
||||
CodeDetails,
|
||||
Contract,
|
||||
ContractDetails,
|
||||
CosmWasmClient,
|
||||
GetNonceResult,
|
||||
PostTxResult,
|
||||
SearchByHeightQuery,
|
||||
SearchByIdQuery,
|
||||
SearchBySentFromOrToQuery,
|
||||
SearchByTagsQuery,
|
||||
SearchTxQuery,
|
||||
SearchTxFilter,
|
||||
} from "./cosmwasmclient";
|
||||
export {
|
||||
ExecuteResult,
|
||||
FeeTable,
|
||||
InstantiateResult,
|
||||
SigningCallback,
|
||||
SigningCosmWasmClient,
|
||||
UploadMeta,
|
||||
UploadResult,
|
||||
} from "./signingcosmwasmclient";
|
||||
export {
|
||||
isMsgExecuteContract,
|
||||
isMsgInstantiateContract,
|
||||
isMsgStoreCode,
|
||||
MsgStoreCode,
|
||||
MsgExecuteContract,
|
||||
MsgInstantiateContract,
|
||||
} from "./msgs";
|
||||
58
packages/cosmwasm/types/msgs.d.ts
vendored
Normal file
58
packages/cosmwasm/types/msgs.d.ts
vendored
Normal file
@ -0,0 +1,58 @@
|
||||
import { Coin, Msg } from "@cosmwasm/sdk38";
|
||||
/**
|
||||
* Uploads Wam code to the chain
|
||||
*
|
||||
* @see https://github.com/cosmwasm/wasmd/blob/9842678d89/x/wasm/internal/types/msg.go#L17
|
||||
*/
|
||||
export interface MsgStoreCode extends Msg {
|
||||
readonly type: "wasm/store-code";
|
||||
readonly value: {
|
||||
/** Bech32 account address */
|
||||
readonly sender: string;
|
||||
/** Base64 encoded Wasm */
|
||||
readonly wasm_byte_code: string;
|
||||
/** A valid URI reference to the contract's source code. Can be empty. */
|
||||
readonly source: string;
|
||||
/** A docker tag. Can be empty. */
|
||||
readonly builder: string;
|
||||
};
|
||||
}
|
||||
/**
|
||||
* Creates an instance of contract that was uploaded before.
|
||||
*
|
||||
* @see https://github.com/cosmwasm/wasmd/blob/9842678d89/x/wasm/internal/types/msg.go#L73
|
||||
*/
|
||||
export interface MsgInstantiateContract extends Msg {
|
||||
readonly type: "wasm/instantiate";
|
||||
readonly value: {
|
||||
/** Bech32 account address */
|
||||
readonly sender: string;
|
||||
/** ID of the Wasm code that was uploaded before */
|
||||
readonly code_id: string;
|
||||
/** Human-readable label for this contract */
|
||||
readonly label: string;
|
||||
/** Init message as JavaScript object */
|
||||
readonly init_msg: any;
|
||||
readonly init_funds: ReadonlyArray<Coin>;
|
||||
};
|
||||
}
|
||||
/**
|
||||
* Creates an instance of contract that was uploaded before.
|
||||
*
|
||||
* @see https://github.com/cosmwasm/wasmd/blob/9842678d89/x/wasm/internal/types/msg.go#L103
|
||||
*/
|
||||
export interface MsgExecuteContract extends Msg {
|
||||
readonly type: "wasm/execute";
|
||||
readonly value: {
|
||||
/** Bech32 account address */
|
||||
readonly sender: string;
|
||||
/** Bech32 account address */
|
||||
readonly contract: string;
|
||||
/** Handle message as JavaScript object */
|
||||
readonly msg: any;
|
||||
readonly sent_funds: ReadonlyArray<Coin>;
|
||||
};
|
||||
}
|
||||
export declare function isMsgStoreCode(msg: Msg): msg is MsgStoreCode;
|
||||
export declare function isMsgInstantiateContract(msg: Msg): msg is MsgInstantiateContract;
|
||||
export declare function isMsgExecuteContract(msg: Msg): msg is MsgExecuteContract;
|
||||
70
packages/cosmwasm/types/restclient.d.ts
vendored
Normal file
70
packages/cosmwasm/types/restclient.d.ts
vendored
Normal 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
packages/cosmwasm/types/types.d.ts
vendored
Normal file
14
packages/cosmwasm/types/types.d.ts
vendored
Normal 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;
|
||||
@ -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",
|
||||
|
||||
@ -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,4 +1,4 @@
|
||||
# @cosmwasm/sdk
|
||||
# @cosmwasm/faucet
|
||||
|
||||
[](https://www.npmjs.com/package/@cosmwasm/faucet)
|
||||
|
||||
|
||||
@ -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
packages/sdk/types/types.d.ts
vendored
123
packages/sdk/types/types.d.ts
vendored
@ -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
packages/sdk38/.eslintignore
Symbolic link
1
packages/sdk38/.eslintignore
Symbolic link
@ -0,0 +1 @@
|
||||
../../.eslintignore
|
||||
3
packages/sdk38/.gitignore
vendored
Normal file
3
packages/sdk38/.gitignore
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
build/
|
||||
dist/
|
||||
docs/
|
||||
12
packages/sdk38/README.md
Normal file
12
packages/sdk38/README.md
Normal file
@ -0,0 +1,12 @@
|
||||
# @cosmwasm/sdk38
|
||||
|
||||
[](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
packages/sdk38/jasmine-testrunner.js
Executable file
26
packages/sdk38/jasmine-testrunner.js
Executable 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
packages/sdk38/karma.conf.js
Normal file
54
packages/sdk38/karma.conf.js
Normal 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
packages/sdk38/nonces/README.txt
Normal file
1
packages/sdk38/nonces/README.txt
Normal file
@ -0,0 +1 @@
|
||||
Directory used to trigger lerna package updates for all packages
|
||||
48
packages/sdk38/package.json
Normal file
48
packages/sdk38/package.json
Normal 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"
|
||||
}
|
||||
}
|
||||
366
packages/sdk38/src/cosmosclient.searchtx.spec.ts
Normal file
366
packages/sdk38/src/cosmosclient.searchtx.spec.ts
Normal file
@ -0,0 +1,366 @@
|
||||
/* eslint-disable @typescript-eslint/camelcase */
|
||||
import { assert, sleep } from "@iov/utils";
|
||||
|
||||
import { Coin } from "./coins";
|
||||
import { CosmosClient } from "./cosmosclient";
|
||||
import { makeSignBytes } from "./encoding";
|
||||
import { Secp256k1Pen } from "./pen";
|
||||
import { RestClient } from "./restclient";
|
||||
import { SigningCosmosClient } from "./signingcosmosclient";
|
||||
import {
|
||||
faucet,
|
||||
fromOneElementArray,
|
||||
makeRandomAddress,
|
||||
pendingWithoutWasmd,
|
||||
wasmd,
|
||||
wasmdEnabled,
|
||||
} from "./testutils.spec";
|
||||
import { CosmosSdkTx, isMsgSend, MsgSend } from "./types";
|
||||
|
||||
describe("CosmosClient.searchTx", () => {
|
||||
let sendSuccessful:
|
||||
| {
|
||||
readonly sender: string;
|
||||
readonly recipient: string;
|
||||
readonly hash: string;
|
||||
readonly height: number;
|
||||
readonly tx: CosmosSdkTx;
|
||||
}
|
||||
| undefined;
|
||||
let sendUnsuccessful:
|
||||
| {
|
||||
readonly sender: string;
|
||||
readonly recipient: string;
|
||||
readonly hash: string;
|
||||
readonly height: number;
|
||||
readonly tx: CosmosSdkTx;
|
||||
}
|
||||
| undefined;
|
||||
|
||||
beforeAll(async () => {
|
||||
if (wasmdEnabled()) {
|
||||
const pen = await Secp256k1Pen.fromMnemonic(faucet.mnemonic);
|
||||
const client = new SigningCosmosClient(wasmd.endpoint, faucet.address, (signBytes) =>
|
||||
pen.sign(signBytes),
|
||||
);
|
||||
|
||||
{
|
||||
const recipient = makeRandomAddress();
|
||||
const transferAmount: Coin = {
|
||||
denom: "ucosm",
|
||||
amount: "1234567",
|
||||
};
|
||||
const result = await client.sendTokens(recipient, [transferAmount]);
|
||||
await sleep(50); // wait until tx is indexed
|
||||
const txDetails = await new RestClient(wasmd.endpoint).txById(result.transactionHash);
|
||||
sendSuccessful = {
|
||||
sender: faucet.address,
|
||||
recipient: recipient,
|
||||
hash: result.transactionHash,
|
||||
height: Number.parseInt(txDetails.height, 10),
|
||||
tx: txDetails.tx,
|
||||
};
|
||||
}
|
||||
|
||||
{
|
||||
const memo = "Sending more than I can afford";
|
||||
const recipient = makeRandomAddress();
|
||||
const transferAmount = [
|
||||
{
|
||||
denom: "ucosm",
|
||||
amount: "123456700000000",
|
||||
},
|
||||
];
|
||||
const sendMsg: MsgSend = {
|
||||
type: "cosmos-sdk/MsgSend",
|
||||
value: {
|
||||
// eslint-disable-next-line @typescript-eslint/camelcase
|
||||
from_address: faucet.address,
|
||||
// eslint-disable-next-line @typescript-eslint/camelcase
|
||||
to_address: recipient,
|
||||
amount: transferAmount,
|
||||
},
|
||||
};
|
||||
const fee = {
|
||||
amount: [
|
||||
{
|
||||
denom: "ucosm",
|
||||
amount: "2000",
|
||||
},
|
||||
],
|
||||
gas: "80000", // 80k
|
||||
};
|
||||
const { accountNumber, sequence } = await client.getNonce();
|
||||
const chainId = await client.getChainId();
|
||||
const signBytes = makeSignBytes([sendMsg], fee, chainId, memo, accountNumber, sequence);
|
||||
const signature = await pen.sign(signBytes);
|
||||
const tx: CosmosSdkTx = {
|
||||
type: "cosmos-sdk/StdTx",
|
||||
value: {
|
||||
msg: [sendMsg],
|
||||
fee: fee,
|
||||
memo: memo,
|
||||
signatures: [signature],
|
||||
},
|
||||
};
|
||||
const transactionId = await client.getIdentifier(tx);
|
||||
const heightBeforeThis = await client.getHeight();
|
||||
try {
|
||||
await client.postTx(tx.value);
|
||||
} catch (error) {
|
||||
// postTx() throws on execution failures, which is a questionable design. Ignore for now.
|
||||
// console.log(error);
|
||||
}
|
||||
sendUnsuccessful = {
|
||||
sender: faucet.address,
|
||||
recipient: recipient,
|
||||
hash: transactionId,
|
||||
height: heightBeforeThis + 1,
|
||||
tx: tx,
|
||||
};
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
describe("with SearchByIdQuery", () => {
|
||||
it("can search successful tx by ID", async () => {
|
||||
pendingWithoutWasmd();
|
||||
assert(sendSuccessful, "value must be set in beforeAll()");
|
||||
const client = new CosmosClient(wasmd.endpoint);
|
||||
const result = await client.searchTx({ id: sendSuccessful.hash });
|
||||
expect(result.length).toEqual(1);
|
||||
expect(result[0]).toEqual(
|
||||
jasmine.objectContaining({
|
||||
height: sendSuccessful.height,
|
||||
hash: sendSuccessful.hash,
|
||||
code: 0,
|
||||
tx: sendSuccessful.tx,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("can search unsuccessful tx by ID", async () => {
|
||||
pendingWithoutWasmd();
|
||||
assert(sendUnsuccessful, "value must be set in beforeAll()");
|
||||
const client = new CosmosClient(wasmd.endpoint);
|
||||
const result = await client.searchTx({ id: sendUnsuccessful.hash });
|
||||
expect(result.length).toEqual(1);
|
||||
expect(result[0]).toEqual(
|
||||
jasmine.objectContaining({
|
||||
height: sendUnsuccessful.height,
|
||||
hash: sendUnsuccessful.hash,
|
||||
code: 5,
|
||||
tx: sendUnsuccessful.tx,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("can search by ID (non existent)", async () => {
|
||||
pendingWithoutWasmd();
|
||||
const client = new CosmosClient(wasmd.endpoint);
|
||||
const nonExistentId = "0000000000000000000000000000000000000000000000000000000000000000";
|
||||
const result = await client.searchTx({ id: nonExistentId });
|
||||
expect(result.length).toEqual(0);
|
||||
});
|
||||
|
||||
it("can search by ID and filter by minHeight", async () => {
|
||||
pendingWithoutWasmd();
|
||||
assert(sendSuccessful);
|
||||
const client = new CosmosClient(wasmd.endpoint);
|
||||
const query = { id: sendSuccessful.hash };
|
||||
|
||||
{
|
||||
const result = await client.searchTx(query, { minHeight: 0 });
|
||||
expect(result.length).toEqual(1);
|
||||
}
|
||||
|
||||
{
|
||||
const result = await client.searchTx(query, { minHeight: sendSuccessful.height - 1 });
|
||||
expect(result.length).toEqual(1);
|
||||
}
|
||||
|
||||
{
|
||||
const result = await client.searchTx(query, { minHeight: sendSuccessful.height });
|
||||
expect(result.length).toEqual(1);
|
||||
}
|
||||
|
||||
{
|
||||
const result = await client.searchTx(query, { minHeight: sendSuccessful.height + 1 });
|
||||
expect(result.length).toEqual(0);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe("with SearchByHeightQuery", () => {
|
||||
it("can search successful tx by height", async () => {
|
||||
pendingWithoutWasmd();
|
||||
assert(sendSuccessful, "value must be set in beforeAll()");
|
||||
const client = new CosmosClient(wasmd.endpoint);
|
||||
const result = await client.searchTx({ height: sendSuccessful.height });
|
||||
expect(result.length).toEqual(1);
|
||||
expect(result[0]).toEqual(
|
||||
jasmine.objectContaining({
|
||||
height: sendSuccessful.height,
|
||||
hash: sendSuccessful.hash,
|
||||
code: 0,
|
||||
tx: sendSuccessful.tx,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("can search unsuccessful tx by height", async () => {
|
||||
pendingWithoutWasmd();
|
||||
assert(sendUnsuccessful, "value must be set in beforeAll()");
|
||||
const client = new CosmosClient(wasmd.endpoint);
|
||||
const result = await client.searchTx({ height: sendUnsuccessful.height });
|
||||
expect(result.length).toEqual(1);
|
||||
expect(result[0]).toEqual(
|
||||
jasmine.objectContaining({
|
||||
height: sendUnsuccessful.height,
|
||||
hash: sendUnsuccessful.hash,
|
||||
code: 5,
|
||||
tx: sendUnsuccessful.tx,
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("with SearchBySentFromOrToQuery", () => {
|
||||
it("can search by sender", async () => {
|
||||
pendingWithoutWasmd();
|
||||
assert(sendSuccessful, "value must be set in beforeAll()");
|
||||
const client = new CosmosClient(wasmd.endpoint);
|
||||
const results = await client.searchTx({ sentFromOrTo: sendSuccessful.sender });
|
||||
expect(results.length).toBeGreaterThanOrEqual(1);
|
||||
|
||||
// Check basic structure of all results
|
||||
for (const result of results) {
|
||||
const containsMsgWithSender = !!result.tx.value.msg.find(
|
||||
(msg) => isMsgSend(msg) && msg.value.from_address == sendSuccessful!.sender,
|
||||
);
|
||||
const containsMsgWithRecipient = !!result.tx.value.msg.find(
|
||||
(msg) => isMsgSend(msg) && msg.value.to_address === sendSuccessful!.sender,
|
||||
);
|
||||
expect(containsMsgWithSender || containsMsgWithRecipient).toEqual(true);
|
||||
}
|
||||
|
||||
// Check details of most recent result
|
||||
expect(results[results.length - 1]).toEqual(
|
||||
jasmine.objectContaining({
|
||||
height: sendSuccessful.height,
|
||||
hash: sendSuccessful.hash,
|
||||
tx: sendSuccessful.tx,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("can search by recipient", async () => {
|
||||
pendingWithoutWasmd();
|
||||
assert(sendSuccessful, "value must be set in beforeAll()");
|
||||
const client = new CosmosClient(wasmd.endpoint);
|
||||
const results = await client.searchTx({ sentFromOrTo: sendSuccessful.recipient });
|
||||
expect(results.length).toBeGreaterThanOrEqual(1);
|
||||
|
||||
// Check basic structure of all results
|
||||
for (const result of results) {
|
||||
const msg = fromOneElementArray(result.tx.value.msg);
|
||||
assert(isMsgSend(msg), `${result.hash} (height ${result.height}) is not a bank send transaction`);
|
||||
expect(
|
||||
msg.value.to_address === sendSuccessful.recipient ||
|
||||
msg.value.from_address == sendSuccessful.recipient,
|
||||
).toEqual(true);
|
||||
}
|
||||
|
||||
// Check details of most recent result
|
||||
expect(results[results.length - 1]).toEqual(
|
||||
jasmine.objectContaining({
|
||||
height: sendSuccessful.height,
|
||||
hash: sendSuccessful.hash,
|
||||
tx: sendSuccessful.tx,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("can search by recipient and filter by minHeight", async () => {
|
||||
pendingWithoutWasmd();
|
||||
assert(sendSuccessful);
|
||||
const client = new CosmosClient(wasmd.endpoint);
|
||||
const query = { sentFromOrTo: sendSuccessful.recipient };
|
||||
|
||||
{
|
||||
const result = await client.searchTx(query, { minHeight: 0 });
|
||||
expect(result.length).toEqual(1);
|
||||
}
|
||||
|
||||
{
|
||||
const result = await client.searchTx(query, { minHeight: sendSuccessful.height - 1 });
|
||||
expect(result.length).toEqual(1);
|
||||
}
|
||||
|
||||
{
|
||||
const result = await client.searchTx(query, { minHeight: sendSuccessful.height });
|
||||
expect(result.length).toEqual(1);
|
||||
}
|
||||
|
||||
{
|
||||
const result = await client.searchTx(query, { minHeight: sendSuccessful.height + 1 });
|
||||
expect(result.length).toEqual(0);
|
||||
}
|
||||
});
|
||||
|
||||
it("can search by recipient and filter by maxHeight", async () => {
|
||||
pendingWithoutWasmd();
|
||||
assert(sendSuccessful);
|
||||
const client = new CosmosClient(wasmd.endpoint);
|
||||
const query = { sentFromOrTo: sendSuccessful.recipient };
|
||||
|
||||
{
|
||||
const result = await client.searchTx(query, { maxHeight: 9999999999999 });
|
||||
expect(result.length).toEqual(1);
|
||||
}
|
||||
|
||||
{
|
||||
const result = await client.searchTx(query, { maxHeight: sendSuccessful.height + 1 });
|
||||
expect(result.length).toEqual(1);
|
||||
}
|
||||
|
||||
{
|
||||
const result = await client.searchTx(query, { maxHeight: sendSuccessful.height });
|
||||
expect(result.length).toEqual(1);
|
||||
}
|
||||
|
||||
{
|
||||
const result = await client.searchTx(query, { maxHeight: sendSuccessful.height - 1 });
|
||||
expect(result.length).toEqual(0);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe("with SearchByTagsQuery", () => {
|
||||
it("can search by transfer.recipient", async () => {
|
||||
pendingWithoutWasmd();
|
||||
assert(sendSuccessful, "value must be set in beforeAll()");
|
||||
const client = new CosmosClient(wasmd.endpoint);
|
||||
const results = await client.searchTx({
|
||||
tags: [{ key: "transfer.recipient", value: sendSuccessful.recipient }],
|
||||
});
|
||||
expect(results.length).toBeGreaterThanOrEqual(1);
|
||||
|
||||
// Check basic structure of all results
|
||||
for (const result of results) {
|
||||
const msg = fromOneElementArray(result.tx.value.msg);
|
||||
assert(isMsgSend(msg), `${result.hash} (height ${result.height}) is not a bank send transaction`);
|
||||
expect(msg.value.to_address).toEqual(sendSuccessful.recipient);
|
||||
}
|
||||
|
||||
// Check details of most recent result
|
||||
expect(results[results.length - 1]).toEqual(
|
||||
jasmine.objectContaining({
|
||||
height: sendSuccessful.height,
|
||||
hash: sendSuccessful.hash,
|
||||
tx: sendSuccessful.tx,
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
235
packages/sdk38/src/cosmosclient.spec.ts
Normal file
235
packages/sdk38/src/cosmosclient.spec.ts
Normal 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
packages/sdk38/src/cosmosclient.ts
Normal file
314
packages/sdk38/src/cosmosclient.ts
Normal 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
packages/sdk38/src/logs.spec.ts
Normal file
165
packages/sdk38/src/logs.spec.ts
Normal 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
packages/sdk38/src/logs.ts
Normal file
86
packages/sdk38/src/logs.ts
Normal 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
packages/sdk38/src/restclient.spec.ts
Normal file
897
packages/sdk38/src/restclient.spec.ts
Normal 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
Loading…
Reference in New Issue
Block a user