Merge pull request #131 from confio/cosmwasm-client-types

CosmWasmClient types
This commit is contained in:
merge-when-green[bot] 2020-03-02 16:42:13 +00:00 committed by GitHub
commit 792e133b29
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
29 changed files with 319 additions and 201 deletions

View File

@ -1,5 +1,5 @@
{
"version": "0.7.0-alpha.2",
"version": "0.7.0-alpha.4",
"useWorkspaces": true,
"npmClient": "yarn"
}

View File

@ -1,6 +1,6 @@
{
"name": "@cosmwasm/bcp",
"version": "0.7.0-alpha.2",
"version": "0.7.0-alpha.4",
"description": "Transaction codec and client to communicate with any wasmd blockchain",
"author": "Ethan Frey <ethanfrey@users.noreply.github.com>",
"license": "Apache-2.0",
@ -38,7 +38,7 @@
"pack-web": "yarn build-or-skip && webpack --mode development --config webpack.web.config.js"
},
"dependencies": {
"@cosmwasm/sdk": "^0.7.0-alpha.2",
"@cosmwasm/sdk": "^0.7.0-alpha.4",
"@iov/bcp": "^2.1.0",
"@iov/crypto": "^2.1.0",
"@iov/encoding": "^2.1.0",

View File

@ -1,22 +1,11 @@
import { Algorithm, PubkeyBytes } from "@iov/bcp";
import { Encoding } from "@iov/encoding";
import { decodeCosmosPubkey, pubkeyToAddress } from "./address";
import { pubkeyToAddress } from "./address";
const { fromBase64, fromHex } = Encoding;
describe("address", () => {
describe("decodeCosmosPubkey", () => {
it("works", () => {
expect(
decodeCosmosPubkey("cosmospub1addwnpepqd8sgxq7aw348ydctp3n5ajufgxp395hksxjzc6565yfp56scupfqhlgyg5"),
).toEqual({
data: fromBase64("A08EGB7ro1ORuFhjOnZcSgwYlpe0DSFjVNUIkNNQxwKQ"),
algo: Algorithm.Secp256k1,
});
});
});
describe("pubkeyToAddress", () => {
it("works for Secp256k1 compressed", () => {
const prefix = "cosmos";

View File

@ -1,21 +1,9 @@
import { decodeBech32Pubkey, pubkeyToAddress as sdkPubkeyToAddress, types } from "@cosmwasm/sdk";
import { Address, Algorithm, PubkeyBundle, PubkeyBytes } from "@iov/bcp";
import { pubkeyToAddress as sdkPubkeyToAddress, types } from "@cosmwasm/sdk";
import { Address, Algorithm, PubkeyBundle } from "@iov/bcp";
import { Secp256k1 } from "@iov/crypto";
import { Encoding } from "@iov/encoding";
const { fromBase64, toBase64 } = Encoding;
export function decodeCosmosPubkey(encodedPubkey: string): PubkeyBundle {
const sdkPubKey = decodeBech32Pubkey(encodedPubkey);
switch (sdkPubKey.type) {
case types.pubkeyType.secp256k1:
return { algo: Algorithm.Secp256k1, data: fromBase64(sdkPubKey.value) as PubkeyBytes };
case types.pubkeyType.ed25519:
return { algo: Algorithm.Ed25519, data: fromBase64(sdkPubKey.value) as PubkeyBytes };
default:
throw new Error("Unsupported Pubkey type: " + sdkPubKey.type);
}
}
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 {

View File

@ -1,5 +1,4 @@
/* eslint-disable @typescript-eslint/camelcase */
import { CosmWasmClient, findSequenceForSignedTx, SearchTxFilter, TxsResponse, types } from "@cosmwasm/sdk";
import { CosmWasmClient, findSequenceForSignedTx, IndexedTx, SearchTxFilter, types } from "@cosmwasm/sdk";
import {
Account,
AccountQuery,
@ -37,10 +36,10 @@ import equal from "fast-deep-equal";
import { ReadonlyDate } from "readonly-date";
import { Producer, Stream } from "xstream";
import { decodeCosmosPubkey, pubkeyToAddress } from "./address";
import { pubkeyToAddress } from "./address";
import { Caip5 } from "./caip5";
import { CosmWasmCodec } from "./cosmwasmcodec";
import { decodeAmount, parseTxsResponseSigned, parseTxsResponseUnsigned } from "./decode";
import { decodeAmount, decodePubkey, parseTxsResponseSigned, parseTxsResponseUnsigned } from "./decode";
import { buildSignedTx } from "./encode";
import { accountToNonce, BankToken, Erc20Token } from "./types";
@ -71,11 +70,11 @@ function deduplicate<T>(input: ReadonlyArray<T>, comparator: (a: T, b: T) => num
}
/** Compares transaxtion by height. If the height is equal, compare by hash to ensure deterministic order */
function compareByHeightAndHash(a: TxsResponse, b: TxsResponse): number {
function compareByHeightAndHash(a: IndexedTx, b: IndexedTx): number {
if (a.height === b.height) {
return a.txhash.localeCompare(b.txhash);
return a.hash.localeCompare(b.hash);
} else {
return parseInt(a.height, 10) - parseInt(b.height, 10);
return a.height - b.height;
}
}
@ -139,8 +138,8 @@ export class CosmWasmConnection implements BlockchainConnection {
}
public async height(): Promise<number> {
const { block } = await this.cosmWasmClient.getBlock();
return parseInt(block.header.height, 10);
const { header } = await this.cosmWasmClient.getBlock();
return header.height;
}
public async getToken(searchTicker: TokenTicker): Promise<Token | undefined> {
@ -165,7 +164,7 @@ export class CosmWasmConnection implements BlockchainConnection {
const address = isPubkeyQuery(query) ? pubkeyToAddress(query.pubkey, this.addressPrefix) : query.address;
const bankAccount = await this.cosmWasmClient.getAccount(address);
const supportedBankCoins = (bankAccount?.coins || []).filter(({ denom }) =>
const supportedBankCoins = (bankAccount?.balance || []).filter(({ denom }) =>
this.bankTokens.find(token => token.denom === denom),
);
const erc20Amounts = await Promise.all(
@ -192,7 +191,7 @@ export class CosmWasmConnection implements BlockchainConnection {
...supportedBankCoins.map(coin => decodeAmount(this.bankTokens, coin)),
...nonZeroErc20Amounts,
].sort((a, b) => a.tokenTicker.localeCompare(b.tokenTicker));
const pubkey = bankAccount?.public_key ? decodeCosmosPubkey(bankAccount.public_key) : undefined;
const pubkey = bankAccount?.pubkey ? decodePubkey(bankAccount.pubkey) : undefined;
return {
address: address,
balance: balance,
@ -248,12 +247,12 @@ export class CosmWasmConnection implements BlockchainConnection {
}
public async getBlockHeader(height: number): Promise<BlockHeader> {
const { block_id, block } = await this.cosmWasmClient.getBlock(height);
const { id, header, txs } = await this.cosmWasmClient.getBlock(height);
return {
id: block_id.hash as BlockId,
height: parseInt(block.header.height, 10),
time: new ReadonlyDate(block.header.time),
transactionCount: block.data.txs?.length || 0,
id: id as BlockId,
height: header.height,
time: new ReadonlyDate(header.time),
transactionCount: txs.length,
};
}
@ -337,13 +336,13 @@ export class CosmWasmConnection implements BlockchainConnection {
const filter: SearchTxFilter = { minHeight: minHeight, maxHeight: maxHeight };
let txs: readonly TxsResponse[];
let txs: readonly IndexedTx[];
if (id) {
txs = await this.cosmWasmClient.searchTx({ id: id }, filter);
} else if (height) {
txs = await this.cosmWasmClient.searchTx({ height: height }, filter);
} else if (sentFromOrTo) {
const pendingRequests = new Array<Promise<readonly TxsResponse[]>>();
const pendingRequests = new Array<Promise<readonly IndexedTx[]>>();
pendingRequests.push(this.cosmWasmClient.searchTx({ sentFromOrTo: sentFromOrTo }, filter));
for (const contract of this.erc20Tokens.map(token => token.contractAddress)) {
const searchBySender = [
@ -371,7 +370,7 @@ export class CosmWasmConnection implements BlockchainConnection {
}
const responses = await Promise.all(pendingRequests);
const allResults = responses.reduce((accumulator, results) => accumulator.concat(results), []);
txs = deduplicate(allResults, (a, b) => a.txhash.localeCompare(b.txhash)).sort(compareByHeightAndHash);
txs = deduplicate(allResults, (a, b) => a.hash.localeCompare(b.hash)).sort(compareByHeightAndHash);
} else {
throw new Error("Unsupported query");
}
@ -460,11 +459,11 @@ export class CosmWasmConnection implements BlockchainConnection {
}
private parseAndPopulateTxResponseUnsigned(
response: TxsResponse,
response: IndexedTx,
): ConfirmedTransaction<UnsignedTransaction> | FailedTransaction {
return parseTxsResponseUnsigned(
this.chainId,
parseInt(response.height, 10),
response.height,
response,
this.bankTokens,
this.erc20Tokens,
@ -472,7 +471,7 @@ export class CosmWasmConnection implements BlockchainConnection {
}
private async parseAndPopulateTxResponseSigned(
response: TxsResponse,
response: IndexedTx,
): Promise<ConfirmedAndSignedTransaction<UnsignedTransaction> | FailedTransaction> {
const firstMsg = response.tx.value.msg.find(() => true);
if (!firstMsg) throw new Error("Got transaction without a first message. What is going on here?");
@ -503,7 +502,7 @@ export class CosmWasmConnection implements BlockchainConnection {
return parseTxsResponseSigned(
this.chainId,
parseInt(response.height, 10),
response.height,
nonce,
response,
this.bankTokens,

View File

@ -1,5 +1,5 @@
/* eslint-disable @typescript-eslint/camelcase */
import { types } from "@cosmwasm/sdk";
import { IndexedTx, types } from "@cosmwasm/sdk";
import { Address, Algorithm, isSendTransaction, SendTransaction, TokenTicker } from "@iov/bcp";
import { Encoding } from "@iov/encoding";
import { assert } from "@iov/utils";
@ -281,10 +281,11 @@ describe("decode", () => {
describe("parseTxsResponseUnsigned", () => {
it("works", () => {
const currentHeight = 2923;
const txsResponse = {
height: "2823",
txhash: testdata.txId,
raw_log: '[{"msg_index":0,"success":true,"log":""}]',
const txsResponse: IndexedTx = {
height: 2823,
hash: testdata.txId,
rawLog: '[{"msg_index":0,"success":true,"log":""}]',
logs: [],
tx: cosmoshub.tx,
timestamp: "2020-02-14T11:35:41Z",
};
@ -310,10 +311,11 @@ describe("decode", () => {
describe("parseTxsResponseSigned", () => {
it("works", () => {
const currentHeight = 2923;
const txsResponse = {
height: "2823",
txhash: testdata.txId,
raw_log: '[{"msg_index":0,"success":true,"log":""}]',
const txsResponse: IndexedTx = {
height: 2823,
hash: testdata.txId,
rawLog: '[{"msg_index":0,"success":true,"log":""}]',
logs: [],
tx: cosmoshub.tx,
timestamp: "2020-02-14T11:35:41Z",
};

View File

@ -1,4 +1,4 @@
import { TxsResponse, types } from "@cosmwasm/sdk";
import { IndexedTx, types } from "@cosmwasm/sdk";
import {
Address,
Algorithm,
@ -180,17 +180,16 @@ export function parseSignedTx(
export function parseTxsResponseUnsigned(
chainId: ChainId,
currentHeight: number,
response: TxsResponse,
response: IndexedTx,
tokens: BankTokens,
erc20Tokens: readonly Erc20Token[],
): ConfirmedTransaction<UnsignedTransaction> {
const height = parseInt(response.height, 10);
return {
transaction: parseUnsignedTx(response.tx.value, chainId, tokens, erc20Tokens),
height: height,
confirmations: currentHeight - height + 1,
transactionId: response.txhash as TransactionId,
log: response.raw_log,
height: response.height,
confirmations: currentHeight - response.height + 1,
transactionId: response.hash as TransactionId,
log: response.rawLog,
};
}
@ -198,16 +197,15 @@ export function parseTxsResponseSigned(
chainId: ChainId,
currentHeight: number,
nonce: Nonce,
response: TxsResponse,
response: IndexedTx,
tokens: BankTokens,
erc20Tokens: readonly Erc20Token[],
): ConfirmedAndSignedTransaction<UnsignedTransaction> {
const height = parseInt(response.height, 10);
return {
...parseSignedTx(response.tx.value, chainId, nonce, tokens, erc20Tokens),
height: height,
confirmations: currentHeight - height + 1,
transactionId: response.txhash as TransactionId,
log: response.raw_log,
height: response.height,
confirmations: currentHeight - response.height + 1,
transactionId: response.hash as TransactionId,
log: response.rawLog,
};
}

View File

@ -1,3 +1,2 @@
import { Address, PubkeyBundle } from "@iov/bcp";
export declare function decodeCosmosPubkey(encodedPubkey: string): PubkeyBundle;
export declare function pubkeyToAddress(pubkey: PubkeyBundle, prefix: string): Address;

View File

@ -1,4 +1,4 @@
import { TxsResponse, types } from "@cosmwasm/sdk";
import { IndexedTx, types } from "@cosmwasm/sdk";
import {
Amount,
ChainId,
@ -43,7 +43,7 @@ export declare function parseSignedTx(
export declare function parseTxsResponseUnsigned(
chainId: ChainId,
currentHeight: number,
response: TxsResponse,
response: IndexedTx,
tokens: BankTokens,
erc20Tokens: readonly Erc20Token[],
): ConfirmedTransaction<UnsignedTransaction>;
@ -51,7 +51,7 @@ export declare function parseTxsResponseSigned(
chainId: ChainId,
currentHeight: number,
nonce: Nonce,
response: TxsResponse,
response: IndexedTx,
tokens: BankTokens,
erc20Tokens: readonly Erc20Token[],
): ConfirmedAndSignedTransaction<UnsignedTransaction>;

View File

@ -1,6 +1,6 @@
{
"name": "@cosmwasm/cli",
"version": "0.7.0-alpha.2",
"version": "0.7.0-alpha.4",
"description": "Command line interface",
"contributors": [
"IOV SAS <admin@iov.one>",
@ -36,7 +36,7 @@
"!**/testdata/"
],
"dependencies": {
"@cosmwasm/sdk": "^0.7.0-alpha.2",
"@cosmwasm/sdk": "^0.7.0-alpha.4",
"@iov/crypto": "^2.1.0",
"@iov/encoding": "^2.1.0",
"@iov/utils": "^2.0.2",

View File

@ -1,6 +1,6 @@
{
"name": "@cosmwasm/faucet",
"version": "0.7.0-alpha.2",
"version": "0.7.0-alpha.4",
"description": "The faucet",
"author": "Ethan Frey <ethanfrey@users.noreply.github.com>",
"license": "Apache-2.0",
@ -35,7 +35,7 @@
"test": "yarn build-or-skip && yarn test-node"
},
"dependencies": {
"@cosmwasm/bcp": "^0.7.0-alpha.2",
"@cosmwasm/bcp": "^0.7.0-alpha.4",
"@iov/bcp": "^2.1.0",
"@iov/crypto": "^2.1.0",
"@iov/encoding": "^2.1.0",

View File

@ -1,6 +1,6 @@
{
"name": "@cosmwasm/sdk",
"version": "0.7.0-alpha.2",
"version": "0.7.0-alpha.4",
"description": "CosmWasm SDK",
"author": "Ethan Frey <ethanfrey@users.noreply.github.com>",
"license": "Apache-2.0",

View File

@ -92,8 +92,8 @@ describe("CosmWasmClient.searchTx", () => {
expect(result.length).toEqual(1);
expect(result[0]).toEqual(
jasmine.objectContaining({
height: postedSend.height.toString(),
txhash: postedSend.hash,
height: postedSend.height,
hash: postedSend.hash,
tx: postedSend.tx,
}),
);
@ -144,8 +144,8 @@ describe("CosmWasmClient.searchTx", () => {
expect(result.length).toEqual(1);
expect(result[0]).toEqual(
jasmine.objectContaining({
height: postedSend.height.toString(),
txhash: postedSend.hash,
height: postedSend.height,
hash: postedSend.hash,
tx: postedSend.tx,
}),
);
@ -163,7 +163,7 @@ describe("CosmWasmClient.searchTx", () => {
// Check basic structure of all results
for (const result of results) {
const msg = fromOneElementArray(result.tx.value.msg);
assert(isMsgSend(msg), `${result.txhash} (height ${result.height}) is not a bank send transaction`);
assert(isMsgSend(msg), `${result.hash} (height ${result.height}) is not a bank send transaction`);
expect(
msg.value.to_address === postedSend.sender || msg.value.from_address == postedSend.sender,
).toEqual(true);
@ -172,8 +172,8 @@ describe("CosmWasmClient.searchTx", () => {
// Check details of most recent result
expect(results[results.length - 1]).toEqual(
jasmine.objectContaining({
height: postedSend.height.toString(),
txhash: postedSend.hash,
height: postedSend.height,
hash: postedSend.hash,
tx: postedSend.tx,
}),
);
@ -189,7 +189,7 @@ describe("CosmWasmClient.searchTx", () => {
// Check basic structure of all results
for (const result of results) {
const msg = fromOneElementArray(result.tx.value.msg);
assert(isMsgSend(msg), `${result.txhash} (height ${result.height}) is not a bank send transaction`);
assert(isMsgSend(msg), `${result.hash} (height ${result.height}) is not a bank send transaction`);
expect(
msg.value.to_address === postedSend.recipient || msg.value.from_address == postedSend.recipient,
).toEqual(true);
@ -198,8 +198,8 @@ describe("CosmWasmClient.searchTx", () => {
// Check details of most recent result
expect(results[results.length - 1]).toEqual(
jasmine.objectContaining({
height: postedSend.height.toString(),
txhash: postedSend.hash,
height: postedSend.height,
hash: postedSend.hash,
tx: postedSend.tx,
}),
);
@ -273,15 +273,15 @@ describe("CosmWasmClient.searchTx", () => {
// Check basic structure of all results
for (const result of results) {
const msg = fromOneElementArray(result.tx.value.msg);
assert(isMsgSend(msg), `${result.txhash} (height ${result.height}) is not a bank send transaction`);
assert(isMsgSend(msg), `${result.hash} (height ${result.height}) is not a bank send transaction`);
expect(msg.value.to_address).toEqual(postedSend.recipient);
}
// Check details of most recent result
expect(results[results.length - 1]).toEqual(
jasmine.objectContaining({
height: postedSend.height.toString(),
txhash: postedSend.hash,
height: postedSend.height,
hash: postedSend.hash,
tx: postedSend.tx,
}),
);
@ -301,7 +301,7 @@ describe("CosmWasmClient.searchTx", () => {
const msg = fromOneElementArray(result.tx.value.msg);
assert(
isMsgExecuteContract(msg) || isMsgInstantiateContract(msg),
`${result.txhash} (at ${result.height}) not an execute or instantiate msg`,
`${result.hash} (at ${result.height}) not an execute or instantiate msg`,
);
}
@ -322,8 +322,8 @@ describe("CosmWasmClient.searchTx", () => {
// Check details of most recent result
expect(results[results.length - 1]).toEqual(
jasmine.objectContaining({
height: postedExecute.height.toString(),
txhash: postedExecute.hash,
height: postedExecute.height,
hash: postedExecute.hash,
tx: postedExecute.tx,
}),
);
@ -344,15 +344,15 @@ describe("CosmWasmClient.searchTx", () => {
// Check basic structure of all results
for (const result of results) {
const msg = fromOneElementArray(result.tx.value.msg);
assert(isMsgExecuteContract(msg), `${result.txhash} (at ${result.height}) not an execute msg`);
assert(isMsgExecuteContract(msg), `${result.hash} (at ${result.height}) not an execute msg`);
expect(msg.value.contract).toEqual(postedExecute.contract);
}
// Check details of most recent result
expect(results[results.length - 1]).toEqual(
jasmine.objectContaining({
height: postedExecute.height.toString(),
txhash: postedExecute.hash,
height: postedExecute.height,
hash: postedExecute.hash,
tx: postedExecute.tx,
}),
);

View File

@ -83,10 +83,10 @@ describe("CosmWasmClient", () => {
const client = new CosmWasmClient(wasmdEndpoint);
expect(await client.getAccount(unused.address)).toEqual({
address: unused.address,
account_number: 5,
accountNumber: 5,
sequence: 0,
public_key: "",
coins: [
pubkey: undefined,
balance: [
{ denom: "ucosm", amount: "1000000000" },
{ denom: "ustake", amount: "1000000000" },
],
@ -108,39 +108,39 @@ describe("CosmWasmClient", () => {
const response = await client.getBlock();
// id
expect(response.block_id.hash).toMatch(tendermintIdMatcher);
expect(response.id).toMatch(tendermintIdMatcher);
// header
expect(parseInt(response.block.header.height, 10)).toBeGreaterThanOrEqual(1);
expect(response.block.header.chain_id).toEqual(await client.chainId());
expect(new ReadonlyDate(response.block.header.time).getTime()).toBeLessThan(ReadonlyDate.now());
expect(new ReadonlyDate(response.block.header.time).getTime()).toBeGreaterThanOrEqual(
expect(response.header.height).toBeGreaterThanOrEqual(1);
expect(response.header.chainId).toEqual(await client.chainId());
expect(new ReadonlyDate(response.header.time).getTime()).toBeLessThan(ReadonlyDate.now());
expect(new ReadonlyDate(response.header.time).getTime()).toBeGreaterThanOrEqual(
ReadonlyDate.now() - 5_000,
);
// data
expect(response.block.data.txs === null || Array.isArray(response.block.data.txs)).toEqual(true);
// txs
expect(Array.isArray(response.txs)).toEqual(true);
});
it("works for block by height", async () => {
pendingWithoutWasmd();
const client = new CosmWasmClient(wasmdEndpoint);
const height = parseInt((await client.getBlock()).block.header.height, 10);
const height = (await client.getBlock()).header.height;
const response = await client.getBlock(height - 1);
// id
expect(response.block_id.hash).toMatch(tendermintIdMatcher);
expect(response.id).toMatch(tendermintIdMatcher);
// header
expect(response.block.header.height).toEqual(`${height - 1}`);
expect(response.block.header.chain_id).toEqual(await client.chainId());
expect(new ReadonlyDate(response.block.header.time).getTime()).toBeLessThan(ReadonlyDate.now());
expect(new ReadonlyDate(response.block.header.time).getTime()).toBeGreaterThanOrEqual(
expect(response.header.height).toEqual(height - 1);
expect(response.header.chainId).toEqual(await client.chainId());
expect(new ReadonlyDate(response.header.time).getTime()).toBeLessThan(ReadonlyDate.now());
expect(new ReadonlyDate(response.header.time).getTime()).toBeGreaterThanOrEqual(
ReadonlyDate.now() - 5_000,
);
// data
expect(response.block.data.txs === null || Array.isArray(response.block.data.txs)).toEqual(true);
// txs
expect(Array.isArray(response.txs)).toEqual(true);
});
});
@ -313,7 +313,7 @@ describe("CosmWasmClient", () => {
);
const { codeId } = await client.upload(getRandomizedHackatom());
const initMsg = { verifier: makeRandomAddress(), beneficiary: makeRandomAddress() };
const contractAddress = await client.instantiate(codeId, initMsg, "random hackatom");
const { contractAddress } = await client.instantiate(codeId, initMsg, "random hackatom");
contract = { initMsg: initMsg, address: contractAddress };
}
});
@ -366,7 +366,7 @@ describe("CosmWasmClient", () => {
);
const { codeId } = await client.upload(getRandomizedHackatom());
const initMsg = { verifier: makeRandomAddress(), beneficiary: makeRandomAddress() };
const contractAddress = await client.instantiate(codeId, initMsg, "a different hackatom");
const { contractAddress } = await client.instantiate(codeId, initMsg, "a different hackatom");
contract = { initMsg: initMsg, address: contractAddress };
}
});

View File

@ -2,14 +2,24 @@ import { Sha256 } from "@iov/crypto";
import { Encoding } from "@iov/encoding";
import { Log, parseLogs } from "./logs";
import { BlockResponse, BroadcastMode, RestClient, TxsResponse } from "./restclient";
import { CosmosSdkAccount, CosmosSdkTx, StdTx } from "./types";
import { decodeBech32Pubkey } from "./pubkey";
import { BroadcastMode, RestClient } from "./restclient";
import { Coin, 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;
@ -92,6 +102,41 @@ 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;
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>;
}
export class CosmWasmClient {
protected readonly restClient: RestClient;
@ -129,15 +174,23 @@ export class CosmWasmClient {
);
}
return {
accountNumber: account.account_number,
accountNumber: account.accountNumber,
sequence: account.sequence,
};
}
public async getAccount(address: string): Promise<CosmosSdkAccount | undefined> {
public async getAccount(address: string): Promise<Account | undefined> {
const account = await this.restClient.authAccounts(address);
const value = account.result.value;
return value.address === "" ? undefined : value;
return value.address === ""
? undefined
: {
address: value.address,
balance: value.coins,
pubkey: value.public_key ? decodeBech32Pubkey(value.public_key) : undefined,
accountNumber: value.account_number,
sequence: value.sequence,
};
}
/**
@ -145,15 +198,23 @@ export class CosmWasmClient {
*
* @param height The height of the block. If undefined, the latest height is used.
*/
public async getBlock(height?: number): Promise<BlockResponse> {
if (height !== undefined) {
return this.restClient.blocks(height);
} else {
return this.restClient.blocksLatest();
}
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 TxsResponse[]> {
public async searchTx(query: SearchTxQuery, filter: SearchTxFilter = {}): Promise<readonly IndexedTx[]> {
const minHeight = filter.minHeight || 0;
const maxHeight = filter.maxHeight || Number.MAX_SAFE_INTEGER;
@ -163,7 +224,7 @@ export class CosmWasmClient {
return `${originalQuery}&tx.minheight=${minHeight}&tx.maxheight=${maxHeight}`;
}
let txs: readonly TxsResponse[];
let txs: readonly IndexedTx[];
if (isSearchByIdQuery(query)) {
txs = await this.txsQuery(`tx.hash=${query.id}`);
} else if (isSearchByHeightQuery(query)) {
@ -180,8 +241,8 @@ export class CosmWasmClient {
const sent = await this.txsQuery(sentQuery);
const received = await this.txsQuery(receivedQuery);
const sentHashes = sent.map(t => t.txhash);
txs = [...sent, ...received.filter(t => !sentHashes.includes(t.txhash))];
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);
@ -190,10 +251,7 @@ export class CosmWasmClient {
}
// backend sometimes messes up with min/max height filtering
const filtered = txs.filter(tx => {
const txHeight = parseInt(tx.height, 10);
return txHeight >= minHeight && txHeight <= maxHeight;
});
const filtered = txs.filter(tx => tx.height >= minHeight && tx.height <= maxHeight);
return filtered;
}
@ -303,7 +361,7 @@ export class CosmWasmClient {
}
}
private async txsQuery(query: string): Promise<readonly TxsResponse[]> {
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}`);
@ -313,6 +371,15 @@ export class CosmWasmClient {
`Found more results on the backend than we can process currently. Results: ${result.total_count}, supported: ${limit}`,
);
}
return result.txs;
return result.txs.map(
(restItem): IndexedTx => ({
height: parseInt(restItem.height, 10),
hash: restItem.txhash,
rawLog: restItem.raw_log,
logs: parseLogs(restItem.logs || []),
tx: restItem.tx,
timestamp: restItem.timestamp,
}),
);
}
}

View File

@ -7,11 +7,16 @@ export { unmarshalTx } from "./decoding";
export { makeSignBytes, marshalTx } from "./encoding";
export { BroadcastMode, RestClient, TxsResponse } from "./restclient";
export {
Account,
Block,
BlockHeader,
Code,
CodeDetails,
Contract,
ContractDetails,
CosmWasmClient,
GetNonceResult,
IndexedTx,
PostTxResult,
SearchByHeightQuery,
SearchByIdQuery,
@ -29,9 +34,10 @@ export {
export { findSequenceForSignedTx } from "./sequence";
export { encodeSecp256k1Signature, decodeSignature } from "./signature";
export {
ExecuteResult,
InstantiateResult,
SigningCallback,
SigningCosmWasmClient,
ExecuteResult,
UploadMeta,
UploadReceipt,
UploadResult,
} from "./signingcosmwasmclient";

View File

@ -1,7 +1,7 @@
import { Bech32, Encoding } from "@iov/encoding";
import equal from "fast-deep-equal";
import { Bech32PubKey, PubKey, pubkeyType } from "./types";
import { PubKey, pubkeyType } from "./types";
export function encodeSecp256k1Pubkey(pubkey: Uint8Array): PubKey {
if (pubkey.length !== 33 || (pubkey[0] !== 0x02 && pubkey[0] !== 0x03)) {
@ -28,8 +28,8 @@ const pubkeyAminoPrefixEd25519 = Encoding.fromHex("1624de6420");
const pubkeyAminoPrefixSr25519 = Encoding.fromHex("0dfb1005");
const pubkeyAminoPrefixLength = pubkeyAminoPrefixSecp256k1.length;
export function decodeBech32Pubkey(bech: Bech32PubKey): PubKey {
const { prefix, data } = Bech32.decode(bech);
export function decodeBech32Pubkey(bechEncoded: string): PubKey {
const { prefix, data } = Bech32.decode(bechEncoded);
if (!isCosmosPubkeyBech32Prefix(prefix)) {
throw new Error(`Invalid bech32 prefix. Must be one of ${validPubkeyPrefixes.join(", ")}.`);
}
@ -65,7 +65,7 @@ export function decodeBech32Pubkey(bech: Bech32PubKey): PubKey {
}
}
export function encodeBech32Pubkey(pubkey: PubKey, prefix: CosmosPubkeyBech32Prefix): Bech32PubKey {
export function encodeBech32Pubkey(pubkey: PubKey, prefix: CosmosPubkeyBech32Prefix): string {
let aminoPrefix: Uint8Array;
switch (pubkey.type) {
// Note: please don't add cases here without writing additional unit tests

View File

@ -1,10 +1,20 @@
import { Encoding } from "@iov/encoding";
import axios, { AxiosError, AxiosInstance } from "axios";
import { CosmosSdkAccount, CosmosSdkTx, Model, parseWasmData, StdTx, WasmData } from "./types";
import { Coin, CosmosSdkTx, Model, parseWasmData, StdTx, WasmData } from "./types";
const { fromBase64, toHex, toUtf8 } = Encoding;
export interface CosmosSdkAccount {
/** Bech32 account address */
readonly address: string;
readonly coins: ReadonlyArray<Coin>;
/** Bech32 encoded pubkey */
readonly public_key: string;
readonly account_number: number;
readonly sequence: number;
}
interface NodeInfo {
readonly network: string;
}

View File

@ -87,7 +87,7 @@ describe("SigningCosmWasmClient", () => {
},
];
const beneficiaryAddress = makeRandomAddress();
const contractAddress = await client.instantiate(
const { contractAddress } = await client.instantiate(
codeId,
{
verifier: faucet.address,
@ -148,7 +148,7 @@ describe("SigningCosmWasmClient", () => {
},
];
const beneficiaryAddress = makeRandomAddress();
const contractAddress = await client.instantiate(
const { contractAddress } = await client.instantiate(
codeId,
{
verifier: faucet.address,
@ -205,7 +205,7 @@ describe("SigningCosmWasmClient", () => {
// got tokens
const after = await client.getAccount(beneficiaryAddress);
assert(after);
expect(after.coins).toEqual(transferAmount);
expect(after.balance).toEqual(transferAmount);
});
});
});

View File

@ -3,13 +3,12 @@ import { Encoding } from "@iov/encoding";
import pako from "pako";
import { isValidBuilder } from "./builder";
import { CosmWasmClient, GetNonceResult, PostTxResult } from "./cosmwasmclient";
import { Account, CosmWasmClient, GetNonceResult, PostTxResult } from "./cosmwasmclient";
import { makeSignBytes } from "./encoding";
import { findAttribute, Log } from "./logs";
import { BroadcastMode } from "./restclient";
import {
Coin,
CosmosSdkAccount,
MsgExecuteContract,
MsgInstantiateContract,
MsgSend,
@ -68,7 +67,7 @@ export interface UploadMeta {
readonly builder?: string;
}
export interface UploadReceipt {
export interface UploadResult {
/** Size of the original wasm code in bytes */
readonly originalSize: number;
/** A hex encoded sha256 checksum of the original wasm code (that is stored on chain) */
@ -79,6 +78,17 @@ export interface UploadReceipt {
readonly compressedChecksum: string;
/** The ID of the code asigned by the chain */
readonly codeId: number;
readonly logs: readonly Log[];
/** Transaction hash (might be used as transaction ID). Guaranteed to be non-empty upper-case hex */
readonly transactionHash: string;
}
export interface InstantiateResult {
/** The address of the newly instantiated contract */
readonly contractAddress: string;
readonly logs: readonly Log[];
/** Transaction hash (might be used as transaction ID). Guaranteed to be non-empty upper-case hex */
readonly transactionHash: string;
}
export interface ExecuteResult {
@ -110,12 +120,12 @@ export class SigningCosmWasmClient extends CosmWasmClient {
return super.getNonce(address || this.senderAddress);
}
public async getAccount(address?: string): Promise<CosmosSdkAccount | undefined> {
public async getAccount(address?: string): Promise<Account | undefined> {
return super.getAccount(address || this.senderAddress);
}
/** Uploads code and returns a receipt, including the code ID */
public async upload(wasmCode: Uint8Array, meta: UploadMeta = {}, memo = ""): Promise<UploadReceipt> {
public async upload(wasmCode: Uint8Array, meta: UploadMeta = {}, memo = ""): Promise<UploadResult> {
const source = meta.source || "";
const builder = prepareBuilder(meta.builder);
@ -150,6 +160,8 @@ export class SigningCosmWasmClient extends CosmWasmClient {
compressedSize: compressed.length,
compressedChecksum: Encoding.toHex(new Sha256(compressed).digest()),
codeId: Number.parseInt(codeIdAttr.value, 10),
logs: result.logs,
transactionHash: result.transactionHash,
};
}
@ -159,7 +171,7 @@ export class SigningCosmWasmClient extends CosmWasmClient {
label: string,
memo = "",
transferAmount?: readonly Coin[],
): Promise<string> {
): Promise<InstantiateResult> {
const instantiateMsg: MsgInstantiateContract = {
type: "wasm/instantiate",
value: {
@ -188,7 +200,11 @@ export class SigningCosmWasmClient extends CosmWasmClient {
const result = await this.postTx(signedTx);
const contractAddressAttr = findAttribute(result.logs, "message", "contract_address");
return contractAddressAttr.value;
return {
contractAddress: contractAddressAttr.value,
logs: result.logs,
transactionHash: result.transactionHash,
};
}
public async execute(

View File

@ -150,18 +150,6 @@ export const pubkeyType = {
export const pubkeyTypes: readonly string[] = [pubkeyType.secp256k1, pubkeyType.ed25519, pubkeyType.sr25519];
// bech32-encoded amino-binary encoded PubKey interface. oof.
export type Bech32PubKey = string;
export interface CosmosSdkAccount {
/** Bech32 account address */
readonly address: string;
readonly coins: ReadonlyArray<Coin>;
readonly public_key: Bech32PubKey;
readonly account_number: number;
readonly sequence: number;
}
export interface WasmData {
// key is hex-encoded
readonly key: string;

View File

@ -1,10 +1,18 @@
import { Log } from "./logs";
import { BlockResponse, BroadcastMode, RestClient, TxsResponse } from "./restclient";
import { CosmosSdkAccount, CosmosSdkTx, StdTx } from "./types";
import { BroadcastMode, RestClient } from "./restclient";
import { Coin, 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;
@ -63,6 +71,38 @@ 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;
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>;
}
export declare class CosmWasmClient {
protected readonly restClient: RestClient;
constructor(url: string, broadcastMode?: BroadcastMode);
@ -79,14 +119,14 @@ export declare class CosmWasmClient {
* @param address returns data for this address. When unset, the client's sender adddress is used.
*/
getNonce(address: string): Promise<GetNonceResult>;
getAccount(address: string): Promise<CosmosSdkAccount | undefined>;
getAccount(address: string): Promise<Account | undefined>;
/**
* Gets block header and meta
*
* @param height The height of the block. If undefined, the latest height is used.
*/
getBlock(height?: number): Promise<BlockResponse>;
searchTx(query: SearchTxQuery, filter?: SearchTxFilter): Promise<readonly TxsResponse[]>;
getBlock(height?: number): Promise<Block>;
searchTx(query: SearchTxQuery, filter?: SearchTxFilter): Promise<readonly IndexedTx[]>;
postTx(tx: StdTx): Promise<PostTxResult>;
getCodes(): Promise<readonly Code[]>;
getCodeDetails(codeId: number): Promise<CodeDetails>;

View File

@ -6,11 +6,16 @@ export { unmarshalTx } from "./decoding";
export { makeSignBytes, marshalTx } from "./encoding";
export { BroadcastMode, RestClient, TxsResponse } from "./restclient";
export {
Account,
Block,
BlockHeader,
Code,
CodeDetails,
Contract,
ContractDetails,
CosmWasmClient,
GetNonceResult,
IndexedTx,
PostTxResult,
SearchByHeightQuery,
SearchByIdQuery,
@ -28,9 +33,10 @@ export {
export { findSequenceForSignedTx } from "./sequence";
export { encodeSecp256k1Signature, decodeSignature } from "./signature";
export {
ExecuteResult,
InstantiateResult,
SigningCallback,
SigningCosmWasmClient,
ExecuteResult,
UploadMeta,
UploadReceipt,
UploadResult,
} from "./signingcosmwasmclient";

View File

@ -1,5 +1,5 @@
import { Bech32PubKey, PubKey } from "./types";
import { PubKey } from "./types";
export declare function encodeSecp256k1Pubkey(pubkey: Uint8Array): PubKey;
export declare type CosmosPubkeyBech32Prefix = "cosmospub" | "cosmosvalconspub" | "cosmosvaloperpub";
export declare function decodeBech32Pubkey(bech: Bech32PubKey): PubKey;
export declare function encodeBech32Pubkey(pubkey: PubKey, prefix: CosmosPubkeyBech32Prefix): Bech32PubKey;
export declare function decodeBech32Pubkey(bechEncoded: string): PubKey;
export declare function encodeBech32Pubkey(pubkey: PubKey, prefix: CosmosPubkeyBech32Prefix): string;

View File

@ -1,4 +1,13 @@
import { CosmosSdkAccount, CosmosSdkTx, Model, StdTx } from "./types";
import { Coin, CosmosSdkTx, Model, StdTx } from "./types";
export interface CosmosSdkAccount {
/** Bech32 account address */
readonly address: string;
readonly coins: ReadonlyArray<Coin>;
/** Bech32 encoded pubkey */
readonly public_key: string;
readonly account_number: number;
readonly sequence: number;
}
interface NodeInfo {
readonly network: string;
}

View File

@ -1,7 +1,7 @@
import { CosmWasmClient, GetNonceResult, PostTxResult } from "./cosmwasmclient";
import { Account, CosmWasmClient, GetNonceResult, PostTxResult } from "./cosmwasmclient";
import { Log } from "./logs";
import { BroadcastMode } from "./restclient";
import { Coin, CosmosSdkAccount, StdFee, StdSignature } from "./types";
import { Coin, StdFee, StdSignature } from "./types";
export interface SigningCallback {
(signBytes: Uint8Array): Promise<StdSignature>;
}
@ -17,7 +17,7 @@ export interface UploadMeta {
/** The builder tag */
readonly builder?: string;
}
export interface UploadReceipt {
export interface UploadResult {
/** Size of the original wasm code in bytes */
readonly originalSize: number;
/** A hex encoded sha256 checksum of the original wasm code (that is stored on chain) */
@ -28,6 +28,16 @@ export interface UploadReceipt {
readonly compressedChecksum: string;
/** The ID of the code asigned by the chain */
readonly codeId: number;
readonly logs: readonly Log[];
/** Transaction hash (might be used as transaction ID). Guaranteed to be non-empty upper-case hex */
readonly transactionHash: string;
}
export interface InstantiateResult {
/** The address of the newly instantiated contract */
readonly contractAddress: string;
readonly logs: readonly Log[];
/** Transaction hash (might be used as transaction ID). Guaranteed to be non-empty upper-case hex */
readonly transactionHash: string;
}
export interface ExecuteResult {
readonly logs: readonly Log[];
@ -46,16 +56,16 @@ export declare class SigningCosmWasmClient extends CosmWasmClient {
broadcastMode?: BroadcastMode,
);
getNonce(address?: string): Promise<GetNonceResult>;
getAccount(address?: string): Promise<CosmosSdkAccount | undefined>;
getAccount(address?: string): Promise<Account | undefined>;
/** Uploads code and returns a receipt, including the code ID */
upload(wasmCode: Uint8Array, meta?: UploadMeta, memo?: string): Promise<UploadReceipt>;
upload(wasmCode: Uint8Array, meta?: UploadMeta, memo?: string): Promise<UploadResult>;
instantiate(
codeId: number,
initMsg: object,
label: string,
memo?: string,
transferAmount?: readonly Coin[],
): Promise<string>;
): Promise<InstantiateResult>;
execute(
contractAddress: string,
handleMsg: object,

View File

@ -109,15 +109,6 @@ export declare const pubkeyType: {
sr25519: "tendermint/PubKeySr25519";
};
export declare const pubkeyTypes: readonly string[];
export declare type Bech32PubKey = string;
export interface CosmosSdkAccount {
/** Bech32 account address */
readonly address: string;
readonly coins: ReadonlyArray<Coin>;
readonly public_key: Bech32PubKey;
readonly account_number: number;
readonly sequence: number;
}
export interface WasmData {
readonly key: string;
readonly val: string;

View File

@ -82,7 +82,7 @@ async function main() {
for (const initMsg of [initMsgHash, initMsgIsa, initMsgJade]) {
const memo = `Create an ERC20 instance for ${initMsg.symbol}`;
const contractAddress = await client.instantiate(uploadReceipt.codeId, initMsg, initMsg.symbol, memo);
const { contractAddress } = await client.instantiate(uploadReceipt.codeId, initMsg, initMsg.symbol, memo);
console.info(`Contract instantiated for ${initMsg.symbol} at ${contractAddress}`);
}
}

View File

@ -45,7 +45,7 @@ async function main() {
for (const { label, initMsg } of [free, luxury]) {
const memo = `Create an nameservice instance "${label}"`;
const contractAddress = await client.instantiate(uploadReceipt.codeId, initMsg, label, memo);
const { contractAddress } = await client.instantiate(uploadReceipt.codeId, initMsg, label, memo);
console.info(`Contract "${label}" instantiated at ${contractAddress}`);
}
}