Create initial LcdClient

This commit is contained in:
Simon Warta 2020-07-07 07:04:54 +02:00
parent 433362608c
commit a63ddb40dd
3 changed files with 1152 additions and 0 deletions

View File

@ -0,0 +1,869 @@
/* eslint-disable @typescript-eslint/camelcase */
import { assert, sleep } from "@cosmjs/utils";
import { rawSecp256k1PubkeyToAddress } from "./address";
import { Coin } from "./coins";
import { isPostTxFailure } from "./cosmosclient";
import { makeSignBytes } from "./encoding";
import { CosmosSdkArray, LcdClient, normalizeArray } from "./lcdclient";
import { parseLogs } from "./logs";
import { Msg, MsgSend } from "./msgs";
import { makeCosmoshubPath, Secp256k1Pen } from "./pen";
import { BroadcastMode, TxsResponse } from "./restclient";
import { SigningCosmosClient } from "./signingcosmosclient";
import cosmoshub from "./testdata/cosmoshub.json";
import {
faucet,
makeRandomAddress,
nonNegativeIntegerMatcher,
pendingWithoutWasmd,
tendermintIdMatcher,
wasmd,
wasmdEnabled,
} from "./testutils.spec";
import { StdFee, StdSignature, StdTx } from "./types";
const emptyAddress = "cosmos1ltkhnmdcqemmd2tkhnx7qx66tq7e0wykw2j85k";
/** Deployed as part of scripts/wasmd/init.sh */
export const deployedErc20 = {
codeId: 1,
source: "https://crates.io/api/v1/crates/cw-erc20/0.5.1/download",
builder: "cosmwasm/rust-optimizer:0.8.0",
checksum: "3e97bf88bd960fee5e5959c77b972eb2927690bc10160792741b174f105ec0c5",
instances: [
"cosmos18vd8fpwxzck93qlwghaj6arh4p7c5n89uzcee5", // HASH
"cosmos1hqrdl6wstt8qzshwc6mrumpjk9338k0lr4dqxd", // ISA
"cosmos18r5szma8hm93pvx6lwpjwyxruw27e0k5uw835c", // JADE
],
};
function makeSignedTx(firstMsg: Msg, fee: StdFee, memo: string, firstSignature: StdSignature): StdTx {
return {
msg: [firstMsg],
fee: fee,
memo: memo,
signatures: [firstSignature],
};
}
describe("LcdClient", () => {
it("can be constructed", () => {
const client = new LcdClient(wasmd.endpoint);
expect(client).toBeTruthy();
});
describe("withModules", () => {
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;
}
// 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;
}
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;
}
interface WasmModule extends Record<string, () => any> {
listCodeInfo: () => Promise<readonly CodeInfo[]>;
}
it("works for no modules", async () => {
const client = LcdClient.withModules(wasmd.endpoint, BroadcastMode.Sync);
expect(client).toBeTruthy();
});
it("works for one module", async () => {
pendingWithoutWasmd();
function wasmClientRegisterer(base: LcdClient): WasmModule {
return {
listCodeInfo: async (): Promise<readonly CodeInfo[]> => {
const path = `/wasm/code`;
const responseData = (await base.get(path)) as WasmResponse<CosmosSdkArray<CodeInfo>>;
return normalizeArray(unwrapWasmResponse(responseData));
},
};
}
const client = LcdClient.withModules(wasmd.endpoint, BroadcastMode.Sync, wasmClientRegisterer);
const codes = await client.listCodeInfo();
expect(codes.length).toBeGreaterThanOrEqual(3);
expect(codes[0].id).toEqual(deployedErc20.codeId);
expect(codes[0].data_hash).toEqual(deployedErc20.checksum.toUpperCase());
expect(codes[0].builder).toEqual(deployedErc20.builder);
expect(codes[0].source).toEqual(deployedErc20.source);
});
it("works for two modules", async () => {
pendingWithoutWasmd();
function registerWasmModule(base: LcdClient): WasmModule {
return {
listCodeInfo: async (): Promise<readonly CodeInfo[]> => {
const path = `/wasm/code`;
const responseData = (await base.get(path)) as WasmResponse<CosmosSdkArray<CodeInfo>>;
return normalizeArray(unwrapWasmResponse(responseData));
},
};
}
interface TotalSupplyReponse {
readonly height: string;
readonly result: CosmosSdkArray<Coin>;
}
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
function registerSupplyModule(base: LcdClient) {
return {
totalSupply: async (): Promise<TotalSupplyReponse> => {
const path = `/supply/total`;
return (await base.get(path)) as TotalSupplyReponse;
},
};
}
const client = LcdClient.withModules(
wasmd.endpoint,
BroadcastMode.Sync,
registerWasmModule,
registerSupplyModule,
);
const codes = await client.listCodeInfo();
expect(codes.length).toBeGreaterThanOrEqual(3);
expect(codes[0].id).toEqual(deployedErc20.codeId);
expect(codes[0].data_hash).toEqual(deployedErc20.checksum.toUpperCase());
expect(codes[0].builder).toEqual(deployedErc20.builder);
expect(codes[0].source).toEqual(deployedErc20.source);
const supply = await client.totalSupply();
expect(supply).toEqual({
height: jasmine.stringMatching(/^[0-9]+$/),
result: [
{
amount: jasmine.stringMatching(/^[0-9]+$/),
denom: "ucosm",
},
{
amount: jasmine.stringMatching(/^[0-9]+$/),
denom: "ustake",
},
],
});
});
});
// 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 });
const result = await client.postTx(signedTx);
assert(isPostTxFailure(result));
unsuccessful = {
sender: faucet.address,
recipient: recipient,
hash: transactionId,
};
}
await sleep(75); // wait until transactions are indexed
}
});
it("works for successful transaction", async () => {
pendingWithoutWasmd();
assert(successful);
const client = new LcdClient(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 LcdClient(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(75); // wait until tx is indexed
const txDetails = await new LcdClient(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 LcdClient(wasmd.endpoint);
const result = await client.txsQuery(`tx.height=${posted.height}&limit=26`);
expect(result).toEqual({
count: jasmine.stringMatching(/^(1|2|3|4|5)$/), // 1-5 transactions as string
limit: "26",
page_number: "1",
page_total: "1",
total_count: jasmine.stringMatching(/^(1|2|3|4|5)$/), // 1-5 transactions as string
txs: jasmine.arrayContaining([posted.tx]),
});
});
it("can query transactions by ID", async () => {
pendingWithoutWasmd();
assert(posted);
const client = new LcdClient(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 LcdClient(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 LcdClient(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 LcdClient(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 LcdClient(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 LcdClient(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 LcdClient(wasmd.endpoint);
const response = await client.encodeTx(cosmoshub.tx);
expect(response).toEqual(
jasmine.objectContaining({
tx: 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 LcdClient(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 LcdClient(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);
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 LcdClient(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);
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 LcdClient(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);
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 LcdClient(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);
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 LcdClient(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);
expect(postResult.code).toEqual(8);
});
});
});

View File

@ -0,0 +1,217 @@
/* eslint-disable no-dupe-class-members */
import { assert, isNonNullObject } from "@cosmjs/utils";
import axios, { AxiosError, AxiosInstance } from "axios";
import {
AuthAccountsResponse,
BlockResponse,
BroadcastMode,
EncodeTxResponse,
NodeInfoResponse,
PostTxsResponse,
SearchTxsResponse,
TxsResponse,
} from "./restclient";
import { CosmosSdkTx, StdTx } from "./types";
/** Unfortunately, Cosmos SDK encodes empty arrays as null */
export type CosmosSdkArray<T> = ReadonlyArray<T> | null;
export function normalizeArray<T>(backend: CosmosSdkArray<T>): ReadonlyArray<T> {
return backend || [];
}
type LcdClientModule = Record<string, () => any>;
type LcdClientModuleRegisterer<M> = (base: LcdClient) => M;
// 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
function parseAxiosError(err: AxiosError): never {
// use the error message sent from server, not default 500 msg
if (err.response?.data) {
let errorText: string;
const data = err.response.data;
// expect { error: string }, but otherwise dump
if (data.error && typeof data.error === "string") {
errorText = data.error;
} else if (typeof data === "string") {
errorText = data;
} else {
errorText = JSON.stringify(data);
}
throw new Error(`${errorText} (HTTP ${err.response.status})`);
} else {
throw err;
}
}
export class LcdClient {
/** Constructs an LCD client with 0 modules */
public static withModules(apiUrl: string, broadcastMode: BroadcastMode): LcdClient;
/** Constructs an LCD client with 1 modules */
public static withModules<A extends LcdClientModule>(
apiUrl: string,
broadcastMode: BroadcastMode,
moduleA: LcdClientModuleRegisterer<A>,
): LcdClient & A;
/** Constructs an LCD client with 2 modules */
public static withModules<A extends LcdClientModule, B extends LcdClientModule>(
apiUrl: string,
broadcastMode: BroadcastMode,
moduleA: LcdClientModuleRegisterer<A>,
moduleB: LcdClientModuleRegisterer<B>,
): LcdClient & A & B;
public static withModules<A extends LcdClientModule, B extends LcdClientModule>(
apiUrl: string,
broadcastMode: BroadcastMode,
moduleA?: LcdClientModuleRegisterer<A>,
moduleB?: LcdClientModuleRegisterer<B>,
): any {
const client = new LcdClient(apiUrl, broadcastMode);
const modules = new Array<LcdClientModule>();
if (moduleA) modules.push(moduleA(client));
if (moduleB) modules.push(moduleB(client));
for (const module of modules) {
assert(isNonNullObject(module), `Module must be a non-null object`);
for (const key in module) {
assert(typeof key == "string", `Found non-string module key: ${key}`);
(client as any)[key] = module[key];
}
}
return client;
}
private readonly client: AxiosInstance;
private readonly broadcastMode: 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
*/
public constructor(apiUrl: string, broadcastMode = BroadcastMode.Block) {
const headers = {
post: { "Content-Type": "application/json" },
};
this.client = axios.create({
baseURL: apiUrl,
headers: headers,
});
this.broadcastMode = broadcastMode;
}
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");
}
return data;
}
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) {
throw new Error("Received null response from server");
}
return data;
}
// The /auth endpoints
public async authAccounts(address: string): Promise<AuthAccountsResponse> {
const path = `/auth/accounts/${address}`;
const responseData = await this.get(path);
if (responseData.result.type !== "cosmos-sdk/Account") {
throw new Error("Unexpected response data format");
}
return responseData as AuthAccountsResponse;
}
// The /blocks endpoints
public async blocksLatest(): Promise<BlockResponse> {
const responseData = await this.get("/blocks/latest");
if (!responseData.block) {
throw new Error("Unexpected response data format");
}
return responseData as BlockResponse;
}
public async blocks(height: number): Promise<BlockResponse> {
const responseData = await this.get(`/blocks/${height}`);
if (!responseData.block) {
throw new Error("Unexpected response data format");
}
return responseData as BlockResponse;
}
// The /node_info endpoint
public async nodeInfo(): Promise<NodeInfoResponse> {
const responseData = await this.get("/node_info");
if (!responseData.node_info) {
throw new Error("Unexpected response data format");
}
return responseData as NodeInfoResponse;
}
// The /txs endpoints
public async txById(id: string): Promise<TxsResponse> {
const responseData = await this.get(`/txs/${id}`);
if (!responseData.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.txs) {
throw new Error("Unexpected response data format");
}
return responseData as SearchTxsResponse;
}
/** returns the amino-encoding of the transaction performed by the server */
public async encodeTx(tx: CosmosSdkTx): Promise<EncodeTxResponse> {
const responseData = await this.post("/txs/encode", tx);
if (!responseData.tx) {
throw new Error("Unexpected response data format");
}
return responseData as EncodeTxResponse;
}
/**
* Broadcasts a signed transaction to into the transaction pool.
* Depending on the RestClient's broadcast mode, this might or might
* wait for checkTx or deliverTx to be executed before returning.
*
* @param tx a signed transaction as StdTx (i.e. not wrapped in type/value container)
*/
public async postTx(tx: StdTx): Promise<PostTxsResponse> {
const params = {
tx: tx,
mode: this.broadcastMode,
};
const responseData = await this.post("/txs", params);
if (!responseData.txhash) {
throw new Error("Unexpected response data format");
}
return responseData as PostTxsResponse;
}
}

66
packages/sdk38/types/lcdclient.d.ts vendored Normal file
View File

@ -0,0 +1,66 @@
import {
AuthAccountsResponse,
BlockResponse,
BroadcastMode,
EncodeTxResponse,
NodeInfoResponse,
PostTxsResponse,
SearchTxsResponse,
TxsResponse,
} from "./restclient";
import { CosmosSdkTx, StdTx } from "./types";
/** Unfortunately, Cosmos SDK encodes empty arrays as null */
export declare type CosmosSdkArray<T> = ReadonlyArray<T> | null;
export declare function normalizeArray<T>(backend: CosmosSdkArray<T>): ReadonlyArray<T>;
declare type LcdClientModule = Record<string, () => any>;
declare type LcdClientModuleRegisterer<M> = (base: LcdClient) => M;
export declare class LcdClient {
/** Constructs an LCD client with 0 modules */
static withModules(apiUrl: string, broadcastMode: BroadcastMode): LcdClient;
/** Constructs an LCD client with 1 modules */
static withModules<A extends LcdClientModule>(
apiUrl: string,
broadcastMode: BroadcastMode,
moduleA: LcdClientModuleRegisterer<A>,
): LcdClient & A;
/** Constructs an LCD client with 2 modules */
static withModules<A extends LcdClientModule, B extends LcdClientModule>(
apiUrl: string,
broadcastMode: BroadcastMode,
moduleA: LcdClientModuleRegisterer<A>,
moduleB: LcdClientModuleRegisterer<B>,
): LcdClient & A & B;
private readonly client;
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<any>;
post(path: string, params: any): Promise<any>;
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>;
/** returns the amino-encoding of the transaction performed by the server */
encodeTx(tx: CosmosSdkTx): Promise<EncodeTxResponse>;
/**
* Broadcasts a signed transaction to into the transaction pool.
* Depending on the RestClient's broadcast mode, this might or might
* wait for checkTx or deliverTx to be executed before returning.
*
* @param tx a signed transaction as StdTx (i.e. not wrapped in type/value container)
*/
postTx(tx: StdTx): Promise<PostTxsResponse>;
}
export {};