Merge pull request #146 from CosmWasm/article-refinements

Various library refinements
This commit is contained in:
Simon Warta 2020-03-18 11:44:15 +01:00 committed by GitHub
commit 6ad597682c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 450 additions and 109 deletions

View File

@ -284,6 +284,7 @@ describe("decode", () => {
const txsResponse: IndexedTx = {
height: 2823,
hash: testdata.txId,
code: 0,
rawLog: '[{"msg_index":0,"success":true,"log":""}]',
logs: [],
tx: cosmoshub.tx,
@ -314,6 +315,7 @@ describe("decode", () => {
const txsResponse: IndexedTx = {
height: 2823,
hash: testdata.txId,
code: 0,
rawLog: '[{"msg_index":0,"success":true,"log":""}]',
logs: [],
tx: cosmoshub.tx,

View File

@ -2,6 +2,7 @@
import { assert, sleep } from "@iov/utils";
import { CosmWasmClient } from "./cosmwasmclient";
import { makeSignBytes } from "./encoding";
import { Secp256k1Pen } from "./pen";
import { RestClient } from "./restclient";
import { SigningCosmWasmClient } from "./signingcosmwasmclient";
@ -14,10 +15,26 @@ import {
wasmd,
wasmdEnabled,
} from "./testutils.spec";
import { Coin, CosmosSdkTx, isMsgExecuteContract, isMsgInstantiateContract, isMsgSend } from "./types";
import {
Coin,
CosmosSdkTx,
isMsgExecuteContract,
isMsgInstantiateContract,
isMsgSend,
MsgSend,
} from "./types";
describe("CosmWasmClient.searchTx", () => {
let postedSend:
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;
@ -51,8 +68,8 @@ describe("CosmWasmClient.searchTx", () => {
};
const result = await client.sendTokens(recipient, [transferAmount]);
await sleep(50); // wait until tx is indexed
const txDetails = await new RestClient(wasmd.endpoint).txsById(result.transactionHash);
postedSend = {
const txDetails = await new RestClient(wasmd.endpoint).txById(result.transactionHash);
sendSuccessful = {
sender: faucet.address,
recipient: recipient,
hash: result.transactionHash,
@ -61,6 +78,64 @@ describe("CosmWasmClient.searchTx", () => {
};
}
{
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,
};
}
{
const hashInstance = deployedErc20.instances[0];
const msg = {
@ -71,7 +146,7 @@ describe("CosmWasmClient.searchTx", () => {
};
const result = await client.execute(hashInstance, msg);
await sleep(50); // wait until tx is indexed
const txDetails = await new RestClient(wasmd.endpoint).txsById(result.transactionHash);
const txDetails = await new RestClient(wasmd.endpoint).txById(result.transactionHash);
postedExecute = {
sender: faucet.address,
contract: hashInstance,
@ -84,17 +159,34 @@ describe("CosmWasmClient.searchTx", () => {
});
describe("with SearchByIdQuery", () => {
it("can search by ID", async () => {
it("can search successful tx by ID", async () => {
pendingWithoutWasmd();
assert(postedSend, "value must be set in beforeAll()");
assert(sendSuccessful, "value must be set in beforeAll()");
const client = new CosmWasmClient(wasmd.endpoint);
const result = await client.searchTx({ id: postedSend.hash });
const result = await client.searchTx({ id: sendSuccessful.hash });
expect(result.length).toEqual(1);
expect(result[0]).toEqual(
jasmine.objectContaining({
height: postedSend.height,
hash: postedSend.hash,
tx: postedSend.tx,
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 CosmWasmClient(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,
}),
);
});
@ -109,9 +201,9 @@ describe("CosmWasmClient.searchTx", () => {
it("can search by ID and filter by minHeight", async () => {
pendingWithoutWasmd();
assert(postedSend);
assert(sendSuccessful);
const client = new CosmWasmClient(wasmd.endpoint);
const query = { id: postedSend.hash };
const query = { id: sendSuccessful.hash };
{
const result = await client.searchTx(query, { minHeight: 0 });
@ -119,34 +211,51 @@ describe("CosmWasmClient.searchTx", () => {
}
{
const result = await client.searchTx(query, { minHeight: postedSend.height - 1 });
const result = await client.searchTx(query, { minHeight: sendSuccessful.height - 1 });
expect(result.length).toEqual(1);
}
{
const result = await client.searchTx(query, { minHeight: postedSend.height });
const result = await client.searchTx(query, { minHeight: sendSuccessful.height });
expect(result.length).toEqual(1);
}
{
const result = await client.searchTx(query, { minHeight: postedSend.height + 1 });
const result = await client.searchTx(query, { minHeight: sendSuccessful.height + 1 });
expect(result.length).toEqual(0);
}
});
});
describe("with SearchByHeightQuery", () => {
it("can search by height", async () => {
it("can search successful tx by height", async () => {
pendingWithoutWasmd();
assert(postedSend, "value must be set in beforeAll()");
assert(sendSuccessful, "value must be set in beforeAll()");
const client = new CosmWasmClient(wasmd.endpoint);
const result = await client.searchTx({ height: postedSend.height });
const result = await client.searchTx({ height: sendSuccessful.height });
expect(result.length).toEqual(1);
expect(result[0]).toEqual(
jasmine.objectContaining({
height: postedSend.height,
hash: postedSend.hash,
tx: postedSend.tx,
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 CosmWasmClient(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,
}),
);
});
@ -155,9 +264,9 @@ describe("CosmWasmClient.searchTx", () => {
describe("with SearchBySentFromOrToQuery", () => {
it("can search by sender", async () => {
pendingWithoutWasmd();
assert(postedSend, "value must be set in beforeAll()");
assert(sendSuccessful, "value must be set in beforeAll()");
const client = new CosmWasmClient(wasmd.endpoint);
const results = await client.searchTx({ sentFromOrTo: postedSend.sender });
const results = await client.searchTx({ sentFromOrTo: sendSuccessful.sender });
expect(results.length).toBeGreaterThanOrEqual(1);
// Check basic structure of all results
@ -165,25 +274,25 @@ describe("CosmWasmClient.searchTx", () => {
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 === postedSend.sender || msg.value.from_address == postedSend.sender,
msg.value.to_address === sendSuccessful.sender || msg.value.from_address == sendSuccessful.sender,
).toEqual(true);
}
// Check details of most recent result
expect(results[results.length - 1]).toEqual(
jasmine.objectContaining({
height: postedSend.height,
hash: postedSend.hash,
tx: postedSend.tx,
height: sendSuccessful.height,
hash: sendSuccessful.hash,
tx: sendSuccessful.tx,
}),
);
});
it("can search by recipient", async () => {
pendingWithoutWasmd();
assert(postedSend, "value must be set in beforeAll()");
assert(sendSuccessful, "value must be set in beforeAll()");
const client = new CosmWasmClient(wasmd.endpoint);
const results = await client.searchTx({ sentFromOrTo: postedSend.recipient });
const results = await client.searchTx({ sentFromOrTo: sendSuccessful.recipient });
expect(results.length).toBeGreaterThanOrEqual(1);
// Check basic structure of all results
@ -191,25 +300,26 @@ describe("CosmWasmClient.searchTx", () => {
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 === postedSend.recipient || msg.value.from_address == postedSend.recipient,
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: postedSend.height,
hash: postedSend.hash,
tx: postedSend.tx,
height: sendSuccessful.height,
hash: sendSuccessful.hash,
tx: sendSuccessful.tx,
}),
);
});
it("can search by recipient and filter by minHeight", async () => {
pendingWithoutWasmd();
assert(postedSend);
assert(sendSuccessful);
const client = new CosmWasmClient(wasmd.endpoint);
const query = { sentFromOrTo: postedSend.recipient };
const query = { sentFromOrTo: sendSuccessful.recipient };
{
const result = await client.searchTx(query, { minHeight: 0 });
@ -217,26 +327,26 @@ describe("CosmWasmClient.searchTx", () => {
}
{
const result = await client.searchTx(query, { minHeight: postedSend.height - 1 });
const result = await client.searchTx(query, { minHeight: sendSuccessful.height - 1 });
expect(result.length).toEqual(1);
}
{
const result = await client.searchTx(query, { minHeight: postedSend.height });
const result = await client.searchTx(query, { minHeight: sendSuccessful.height });
expect(result.length).toEqual(1);
}
{
const result = await client.searchTx(query, { minHeight: postedSend.height + 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(postedSend);
assert(sendSuccessful);
const client = new CosmWasmClient(wasmd.endpoint);
const query = { sentFromOrTo: postedSend.recipient };
const query = { sentFromOrTo: sendSuccessful.recipient };
{
const result = await client.searchTx(query, { maxHeight: 9999999999999 });
@ -244,17 +354,17 @@ describe("CosmWasmClient.searchTx", () => {
}
{
const result = await client.searchTx(query, { maxHeight: postedSend.height + 1 });
const result = await client.searchTx(query, { maxHeight: sendSuccessful.height + 1 });
expect(result.length).toEqual(1);
}
{
const result = await client.searchTx(query, { maxHeight: postedSend.height });
const result = await client.searchTx(query, { maxHeight: sendSuccessful.height });
expect(result.length).toEqual(1);
}
{
const result = await client.searchTx(query, { maxHeight: postedSend.height - 1 });
const result = await client.searchTx(query, { maxHeight: sendSuccessful.height - 1 });
expect(result.length).toEqual(0);
}
});
@ -263,10 +373,10 @@ describe("CosmWasmClient.searchTx", () => {
describe("with SearchByTagsQuery", () => {
it("can search by transfer.recipient", async () => {
pendingWithoutWasmd();
assert(postedSend, "value must be set in beforeAll()");
assert(sendSuccessful, "value must be set in beforeAll()");
const client = new CosmWasmClient(wasmd.endpoint);
const results = await client.searchTx({
tags: [{ key: "transfer.recipient", value: postedSend.recipient }],
tags: [{ key: "transfer.recipient", value: sendSuccessful.recipient }],
});
expect(results.length).toBeGreaterThanOrEqual(1);
@ -274,15 +384,15 @@ describe("CosmWasmClient.searchTx", () => {
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(postedSend.recipient);
expect(msg.value.to_address).toEqual(sendSuccessful.recipient);
}
// Check details of most recent result
expect(results[results.length - 1]).toEqual(
jasmine.objectContaining({
height: postedSend.height,
hash: postedSend.hash,
tx: postedSend.tx,
height: sendSuccessful.height,
hash: sendSuccessful.hash,
tx: sendSuccessful.tx,
}),
);
});

View File

@ -52,7 +52,7 @@ describe("CosmWasmClient", () => {
it("works", async () => {
pendingWithoutWasmd();
const client = new CosmWasmClient(wasmd.endpoint);
expect(await client.getChainId()).toEqual(wasmd.expectedChainId);
expect(await client.getChainId()).toEqual(wasmd.chainId);
});
it("caches chain ID", async () => {
@ -61,8 +61,8 @@ describe("CosmWasmClient", () => {
const openedClient = (client as unknown) as PrivateCosmWasmClient;
const getCodeSpy = spyOn(openedClient.restClient, "nodeInfo").and.callThrough();
expect(await client.getChainId()).toEqual(wasmd.expectedChainId); // from network
expect(await client.getChainId()).toEqual(wasmd.expectedChainId); // from cache
expect(await client.getChainId()).toEqual(wasmd.chainId); // from network
expect(await client.getChainId()).toEqual(wasmd.chainId); // from cache
expect(getCodeSpy).toHaveBeenCalledTimes(1);
});

View File

@ -107,6 +107,8 @@ 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;
@ -150,8 +152,17 @@ export class CosmWasmClient {
private readonly codesCache = new Map<number, CodeDetails>();
private chainId: string | undefined;
public constructor(url: string, broadcastMode = BroadcastMode.Block) {
this.restClient = new RestClient(url, broadcastMode);
/**
* 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> {
@ -413,6 +424,7 @@ export class CosmWasmClient {
(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,

View File

@ -18,10 +18,10 @@ import {
fromOneElementArray,
getHackatom,
makeRandomAddress,
nonNegativeIntegerMatcher,
pendingWithoutWasmd,
semverMatcher,
tendermintAddressMatcher,
tendermintHeightMatcher,
tendermintIdMatcher,
tendermintOptionalIdMatcher,
tendermintShortHashMatcher,
@ -44,7 +44,6 @@ import {
const { fromAscii, fromBase64, fromHex, toAscii, toBase64, toHex } = Encoding;
const defaultNetworkId = "testing";
const emptyAddress = "cosmos1ltkhnmdcqemmd2tkhnx7qx66tq7e0wykw2j85k";
const unusedAccount = {
address: "cosmos1cjsxept9rkggzxztslae9ndgpdyt2408lk850u",
@ -85,7 +84,7 @@ async function uploadCustomContract(
};
const { account_number, sequence } = (await client.authAccounts(faucet.address)).result.value;
const signBytes = makeSignBytes([theMsg], fee, defaultNetworkId, memo, account_number, sequence);
const signBytes = makeSignBytes([theMsg], fee, wasmd.chainId, memo, account_number, sequence);
const signature = await pen.sign(signBytes);
const signedTx = makeSignedTx(theMsg, fee, memo, signature);
return client.postTx(signedTx);
@ -127,7 +126,7 @@ async function instantiateContract(
};
const { account_number, sequence } = (await client.authAccounts(faucet.address)).result.value;
const signBytes = makeSignBytes([theMsg], fee, defaultNetworkId, memo, account_number, sequence);
const signBytes = makeSignBytes([theMsg], fee, wasmd.chainId, memo, account_number, sequence);
const signature = await pen.sign(signBytes);
const signedTx = makeSignedTx(theMsg, fee, memo, signature);
return client.postTx(signedTx);
@ -159,7 +158,7 @@ async function executeContract(
};
const { account_number, sequence } = (await client.authAccounts(faucet.address)).result.value;
const signBytes = makeSignBytes([theMsg], fee, defaultNetworkId, memo, account_number, sequence);
const signBytes = makeSignBytes([theMsg], fee, wasmd.chainId, memo, account_number, sequence);
const signature = await pen.sign(signBytes);
const signedTx = makeSignedTx(theMsg, fee, memo, signature);
return client.postTx(signedTx);
@ -178,7 +177,7 @@ describe("RestClient", () => {
pendingWithoutWasmd();
const client = new RestClient(wasmd.endpoint);
const { height, result } = await client.authAccounts(unusedAccount.address);
expect(height).toMatch(tendermintHeightMatcher);
expect(height).toMatch(nonNegativeIntegerMatcher);
expect(result).toEqual({
type: "cosmos-sdk/Account",
value: {
@ -239,7 +238,7 @@ describe("RestClient", () => {
// 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(defaultNetworkId);
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,
@ -273,7 +272,7 @@ describe("RestClient", () => {
// 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(defaultNetworkId);
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,
@ -306,10 +305,10 @@ describe("RestClient", () => {
protocol_version: { p2p: "7", block: "10", app: "0" },
id: jasmine.stringMatching(tendermintShortHashMatcher),
listen_addr: "tcp://0.0.0.0:26656",
network: defaultNetworkId,
network: wasmd.chainId,
version: "0.33.0",
channels: "4020212223303800",
moniker: defaultNetworkId,
moniker: wasmd.chainId,
other: { tx_index: "on", rpc_address: "tcp://0.0.0.0:26657" },
});
expect(application_version).toEqual({
@ -326,6 +325,149 @@ describe("RestClient", () => {
// 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 SigningCosmWasmClient(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:
| {
@ -354,7 +496,7 @@ describe("RestClient", () => {
const result = await client.sendTokens(recipient, transferAmount);
await sleep(50); // wait until tx is indexed
const txDetails = await new RestClient(wasmd.endpoint).txsById(result.transactionHash);
const txDetails = await new RestClient(wasmd.endpoint).txById(result.transactionHash);
posted = {
sender: faucet.address,
recipient: recipient,
@ -370,12 +512,14 @@ describe("RestClient", () => {
assert(posted);
const client = new RestClient(wasmd.endpoint);
const result = await client.txsQuery(`tx.height=${posted.height}&limit=26`);
expect(parseInt(result.count, 10)).toEqual(1);
expect(parseInt(result.limit, 10)).toEqual(26);
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).toEqual([posted.tx]);
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 () => {
@ -383,12 +527,14 @@ describe("RestClient", () => {
assert(posted);
const client = new RestClient(wasmd.endpoint);
const result = await client.txsQuery(`tx.hash=${posted.hash}&limit=26`);
expect(parseInt(result.count, 10)).toEqual(1);
expect(parseInt(result.limit, 10)).toEqual(26);
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).toEqual([posted.tx]);
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 () => {
@ -645,12 +791,20 @@ describe("RestClient", () => {
const client = new RestClient(wasmd.endpoint);
const { account_number, sequence } = (await client.authAccounts(faucet.address)).result.value;
const signBytes = makeSignBytes([theMsg], fee, defaultNetworkId, memo, account_number, sequence);
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);
// console.log("Raw log:", result.raw_log);
expect(result.code).toBeFalsy();
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 upload, instantiate and execute wasm", async () => {

View File

@ -1,4 +1,4 @@
import { Encoding } from "@iov/encoding";
import { Encoding, isNonNullObject } from "@iov/encoding";
import axios, { AxiosError, AxiosInstance } from "axios";
import { Coin, CosmosSdkTx, Model, parseWasmData, StdTx, WasmData } from "./types";
@ -119,6 +119,10 @@ interface WasmError {
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;
@ -138,8 +142,6 @@ interface SearchTxsResponse {
readonly txs: readonly TxsResponse[];
}
interface PostTxsParams {}
export interface PostTxsResponse {
readonly height: string;
readonly txhash: string;
@ -263,17 +265,28 @@ function parseAxiosError(err: AxiosError): never {
export class RestClient {
private readonly client: AxiosInstance;
private readonly mode: BroadcastMode;
private readonly broadcastMode: BroadcastMode;
public constructor(url: string, mode = BroadcastMode.Block) {
/**
* 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) {
const headers = {
post: { "Content-Type": "application/json" },
};
this.client = axios.create({
baseURL: url,
baseURL: apiUrl,
headers: headers,
});
this.mode = mode;
this.broadcastMode = broadcastMode;
}
public async get(path: string): Promise<RestClientResponse> {
@ -284,7 +297,8 @@ export class RestClient {
return data;
}
public async post(path: string, params: PostTxsParams): Promise<RestClientResponse> {
public async post(path: string, params: any): Promise<RestClientResponse> {
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) {
throw new Error("Received null response from server");
@ -333,6 +347,14 @@ export class RestClient {
// The /txs endpoints
public async txById(id: string): Promise<TxsResponse> {
const responseData = await this.get(`/txs/${id}`);
if (!(responseData as any).tx) {
throw new Error("Unexpected response data format");
}
return responseData as TxsResponse;
}
public async txsQuery(query: string): Promise<SearchTxsResponse> {
const responseData = await this.get(`/txs?${query}`);
if (!(responseData as any).txs) {
@ -341,14 +363,6 @@ export class RestClient {
return responseData as SearchTxsResponse;
}
public async txsById(id: string): Promise<TxsResponse> {
const responseData = await this.get(`/txs/${id}`);
if (!(responseData as any).tx) {
throw new Error("Unexpected response data format");
}
return responseData as TxsResponse;
}
/** 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);
@ -368,7 +382,7 @@ export class RestClient {
public async postTx(tx: StdTx): Promise<PostTxsResponse> {
const params = {
tx: tx,
mode: this.mode,
mode: this.broadcastMode,
};
const responseData = await this.post("/txs", params);
if (!(responseData as any).txhash) {

View File

@ -103,14 +103,26 @@ export class SigningCosmWasmClient extends CosmWasmClient {
private readonly signCallback: SigningCallback;
private readonly fees: FeeTable;
/**
* Creates a new client with signing capability to interact with a CosmWasm blockchain. This is the bigger brother of CosmWasmClient.
*
* This instance does a lot of caching. In order to benefit from that you should try to use one instance
* for the lifetime of your application. When switching backends, a new instance must be created.
*
* @param apiUrl The URL of a Cosmos SDK light client daemon API (sometimes called REST server or REST API)
* @param senderAddress The address that will sign and send transactions using this instance
* @param signCallback An asynchonous callback to create a signature for a given transaction. This can be implemented using secure key stores that require user interaction.
* @param customFees The fees that are paid for transactions
* @param broadcastMode Defines at which point of the transaction processing the postTx method (i.e. transaction broadcasting) returns
*/
public constructor(
url: string,
apiUrl: string,
senderAddress: string,
signCallback: SigningCallback,
customFees?: Partial<FeeTable>,
broadcastMode = BroadcastMode.Block,
) {
super(url, broadcastMode);
super(apiUrl, broadcastMode);
this.anyValidAddress = senderAddress;
this.senderAddress = senderAddress;

View File

@ -11,7 +11,7 @@ export function makeRandomAddress(): string {
return Bech32.encode("cosmos", Random.getBytes(20));
}
export const tendermintHeightMatcher = /^[0-9]+$/;
export const nonNegativeIntegerMatcher = /^[0-9]+$/;
export const tendermintIdMatcher = /^[0-9A-F]{64}$/;
export const tendermintOptionalIdMatcher = /^([0-9A-F]{64}|)$/;
export const tendermintAddressMatcher = /^[0-9A-F]{40}$/;
@ -36,7 +36,7 @@ export const deployedErc20 = {
export const wasmd = {
endpoint: "http://localhost:1317",
expectedChainId: "testing",
chainId: "testing",
};
export const faucet = {

View File

@ -76,6 +76,8 @@ 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;
@ -113,7 +115,16 @@ export declare class CosmWasmClient {
protected anyValidAddress: string | undefined;
private readonly codesCache;
private chainId;
constructor(url: string, broadcastMode?: BroadcastMode);
/**
* Creates a new client to interact with a CosmWasm blockchain.
*
* This instance does a lot of caching. In order to benefit from that you should try to use one instance
* for the lifetime of your application. When switching backends, a new instance must be created.
*
* @param apiUrl The URL of a Cosmos SDK light client daemon API (sometimes called REST server or REST API)
* @param broadcastMode Defines at which point of the transaction processing the postTx method (i.e. transaction broadcasting) returns
*/
constructor(apiUrl: string, broadcastMode?: BroadcastMode);
getChainId(): Promise<string>;
getHeight(): Promise<number>;
/**

View File

@ -93,6 +93,10 @@ interface WasmError {
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;
@ -110,7 +114,6 @@ interface SearchTxsResponse {
readonly limit: string;
readonly txs: readonly TxsResponse[];
}
interface PostTxsParams {}
export interface PostTxsResponse {
readonly height: string;
readonly txhash: string;
@ -178,16 +181,27 @@ export declare enum BroadcastMode {
}
export declare class RestClient {
private readonly client;
private readonly mode;
constructor(url: string, mode?: BroadcastMode);
private readonly broadcastMode;
/**
* 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);
get(path: string): Promise<RestClientResponse>;
post(path: string, params: PostTxsParams): Promise<RestClientResponse>;
post(path: string, params: any): Promise<RestClientResponse>;
authAccounts(address: string): Promise<AuthAccountsResponse>;
blocksLatest(): Promise<BlockResponse>;
blocks(height: number): Promise<BlockResponse>;
nodeInfo(): Promise<NodeInfoResponse>;
txById(id: string): Promise<TxsResponse>;
txsQuery(query: string): Promise<SearchTxsResponse>;
txsById(id: string): Promise<TxsResponse>;
/** returns the amino-encoding of the transaction performed by the server */
encodeTx(tx: CosmosSdkTx): Promise<Uint8Array>;
/**

View File

@ -48,8 +48,20 @@ export declare class SigningCosmWasmClient extends CosmWasmClient {
readonly senderAddress: string;
private readonly signCallback;
private readonly fees;
/**
* Creates a new client with signing capability to interact with a CosmWasm blockchain. This is the bigger brother of CosmWasmClient.
*
* This instance does a lot of caching. In order to benefit from that you should try to use one instance
* for the lifetime of your application. When switching backends, a new instance must be created.
*
* @param apiUrl The URL of a Cosmos SDK light client daemon API (sometimes called REST server or REST API)
* @param senderAddress The address that will sign and send transactions using this instance
* @param signCallback An asynchonous callback to create a signature for a given transaction. This can be implemented using secure key stores that require user interaction.
* @param customFees The fees that are paid for transactions
* @param broadcastMode Defines at which point of the transaction processing the postTx method (i.e. transaction broadcasting) returns
*/
constructor(
url: string,
apiUrl: string,
senderAddress: string,
signCallback: SigningCallback,
customFees?: Partial<FeeTable>,