Split CosmWasmClient and SigningCosmWasmClient

This commit is contained in:
Simon Warta 2020-02-17 13:51:48 +01:00
parent b6b29562cb
commit 6b522e5b4d
10 changed files with 476 additions and 451 deletions

View File

@ -73,7 +73,7 @@ export class CosmWasmConnection implements BlockchainConnection {
addressPrefix: CosmosAddressBech32Prefix,
tokens: TokenConfiguration,
): Promise<CosmWasmConnection> {
const cosmWasmClient = CosmWasmClient.makeReadOnly(url);
const cosmWasmClient = new CosmWasmClient(url);
const chainData = await this.initialize(cosmWasmClient);
return new CosmWasmConnection(cosmWasmClient, chainData, addressPrefix, tokens);
}

View File

@ -8,24 +8,21 @@ import { makeSignBytes, marshalTx } from "./encoding";
import { findAttribute } from "./logs";
import { Secp256k1Pen } from "./pen";
import { RestClient } from "./restclient";
import { SigningCosmWasmClient } from "./signingcosmwasmclient";
import cosmoshub from "./testdata/cosmoshub.json";
import { getRandomizedHackatom, makeRandomAddress, tendermintIdMatcher } from "./testutils.spec";
import { Coin, CosmosSdkTx, MsgSend, StdFee } from "./types";
import {
cosmosEnabled,
getRandomizedHackatom,
makeRandomAddress,
pendingWithoutCosmos,
tendermintIdMatcher,
} from "./testutils.spec";
import { CosmosSdkTx, MsgSend, StdFee } from "./types";
const { fromAscii, fromUtf8, toAscii } = Encoding;
const httpUrl = "http://localhost:1317";
function cosmosEnabled(): boolean {
return !!process.env.COSMOS_ENABLED;
}
function pendingWithoutCosmos(): void {
if (!cosmosEnabled()) {
return pending("Set COSMOS_ENABLED to enable Cosmos node-based tests");
}
}
const faucet = {
mnemonic:
"economy stock theory fatal elder harbor betray wasp final emotion task crumble siren bottom lizard educate guess current outdoor pair theory focus wife stone",
@ -51,7 +48,7 @@ interface HackatomInstance {
describe("CosmWasmClient", () => {
describe("makeReadOnly", () => {
it("can be constructed", () => {
const client = CosmWasmClient.makeReadOnly(httpUrl);
const client = new CosmWasmClient(httpUrl);
expect(client).toBeTruthy();
});
});
@ -59,7 +56,7 @@ describe("CosmWasmClient", () => {
describe("chainId", () => {
it("works", async () => {
pendingWithoutCosmos();
const client = CosmWasmClient.makeReadOnly(httpUrl);
const client = new CosmWasmClient(httpUrl);
expect(await client.chainId()).toEqual("testing");
});
});
@ -67,7 +64,7 @@ describe("CosmWasmClient", () => {
describe("getNonce", () => {
it("works", async () => {
pendingWithoutCosmos();
const client = CosmWasmClient.makeReadOnly(httpUrl);
const client = new CosmWasmClient(httpUrl);
expect(await client.getNonce(unusedAccount.address)).toEqual({
accountNumber: 5,
sequence: 0,
@ -76,7 +73,7 @@ describe("CosmWasmClient", () => {
it("throws for missing accounts", async () => {
pendingWithoutCosmos();
const client = CosmWasmClient.makeReadOnly(httpUrl);
const client = new CosmWasmClient(httpUrl);
const missing = makeRandomAddress();
await client.getNonce(missing).then(
() => fail("this must not succeed"),
@ -88,7 +85,7 @@ describe("CosmWasmClient", () => {
describe("getAccount", () => {
it("works", async () => {
pendingWithoutCosmos();
const client = CosmWasmClient.makeReadOnly(httpUrl);
const client = new CosmWasmClient(httpUrl);
expect(await client.getAccount(unusedAccount.address)).toEqual({
address: unusedAccount.address,
account_number: 5,
@ -103,7 +100,7 @@ describe("CosmWasmClient", () => {
it("returns undefined for missing accounts", async () => {
pendingWithoutCosmos();
const client = CosmWasmClient.makeReadOnly(httpUrl);
const client = new CosmWasmClient(httpUrl);
const missing = makeRandomAddress();
expect(await client.getAccount(missing)).toBeUndefined();
});
@ -112,7 +109,7 @@ describe("CosmWasmClient", () => {
describe("getBlock", () => {
it("works for latest block", async () => {
pendingWithoutCosmos();
const client = CosmWasmClient.makeReadOnly(httpUrl);
const client = new CosmWasmClient(httpUrl);
const response = await client.getBlock();
// id
@ -132,7 +129,7 @@ describe("CosmWasmClient", () => {
it("works for block by height", async () => {
pendingWithoutCosmos();
const client = CosmWasmClient.makeReadOnly(httpUrl);
const client = new CosmWasmClient(httpUrl);
const height = parseInt((await client.getBlock()).block.header.height, 10);
const response = await client.getBlock(height - 1);
@ -155,7 +152,7 @@ describe("CosmWasmClient", () => {
describe("getIdentifier", () => {
it("works", async () => {
pendingWithoutCosmos();
const client = CosmWasmClient.makeReadOnly(httpUrl);
const client = new CosmWasmClient(httpUrl);
expect(await client.getIdentifier(cosmoshub.tx)).toEqual(cosmoshub.id);
});
});
@ -164,7 +161,7 @@ describe("CosmWasmClient", () => {
it("works", async () => {
pendingWithoutCosmos();
const pen = await Secp256k1Pen.fromMnemonic(faucet.mnemonic);
const client = CosmWasmClient.makeReadOnly(httpUrl);
const client = new CosmWasmClient(httpUrl);
const memo = "My first contract on chain";
const sendMsg: MsgSend = {
@ -222,7 +219,7 @@ describe("CosmWasmClient", () => {
beforeAll(async () => {
if (cosmosEnabled()) {
const pen = await Secp256k1Pen.fromMnemonic(faucet.mnemonic);
const client = CosmWasmClient.makeWritable(httpUrl, faucet.address, signBytes => pen.sign(signBytes));
const client = new SigningCosmWasmClient(httpUrl, faucet.address, signBytes => pen.sign(signBytes));
const recipient = makeRandomAddress();
const transferAmount = [
@ -248,7 +245,7 @@ describe("CosmWasmClient", () => {
it("can search by ID", async () => {
pendingWithoutCosmos();
assert(posted, "value must be set in beforeAll()");
const client = CosmWasmClient.makeReadOnly(httpUrl);
const client = new CosmWasmClient(httpUrl);
const result = await client.searchTx({ id: posted.hash });
expect(result.length).toEqual(1);
expect(result[0]).toEqual(
@ -262,7 +259,7 @@ describe("CosmWasmClient", () => {
it("can search by ID (non existent)", async () => {
pendingWithoutCosmos();
const client = CosmWasmClient.makeReadOnly(httpUrl);
const client = new CosmWasmClient(httpUrl);
const nonExistentId = "0000000000000000000000000000000000000000000000000000000000000000";
const result = await client.searchTx({ id: nonExistentId });
expect(result.length).toEqual(0);
@ -271,7 +268,7 @@ describe("CosmWasmClient", () => {
it("can search by height", async () => {
pendingWithoutCosmos();
assert(posted, "value must be set in beforeAll()");
const client = CosmWasmClient.makeReadOnly(httpUrl);
const client = new CosmWasmClient(httpUrl);
const result = await client.searchTx({ height: posted.height });
expect(result.length).toEqual(1);
expect(result[0]).toEqual(
@ -286,7 +283,7 @@ describe("CosmWasmClient", () => {
it("can search by sender", async () => {
pendingWithoutCosmos();
assert(posted, "value must be set in beforeAll()");
const client = CosmWasmClient.makeReadOnly(httpUrl);
const client = new CosmWasmClient(httpUrl);
const result = await client.searchTx({ sentFromOrTo: posted.sender });
expect(result.length).toBeGreaterThanOrEqual(1);
expect(result[result.length - 1]).toEqual(
@ -301,7 +298,7 @@ describe("CosmWasmClient", () => {
it("can search by recipient", async () => {
pendingWithoutCosmos();
assert(posted, "value must be set in beforeAll()");
const client = CosmWasmClient.makeReadOnly(httpUrl);
const client = new CosmWasmClient(httpUrl);
const result = await client.searchTx({ sentFromOrTo: posted.recipient });
expect(result.length).toBeGreaterThanOrEqual(1);
expect(result[result.length - 1]).toEqual(
@ -314,141 +311,6 @@ describe("CosmWasmClient", () => {
});
});
describe("upload", () => {
it("works", async () => {
pendingWithoutCosmos();
const pen = await Secp256k1Pen.fromMnemonic(faucet.mnemonic);
const client = CosmWasmClient.makeWritable(httpUrl, faucet.address, signBytes => pen.sign(signBytes));
const codeId = await client.upload(getRandomizedHackatom());
expect(codeId).toBeGreaterThanOrEqual(1);
});
});
describe("instantiate", () => {
it("works with transfer amount", async () => {
pendingWithoutCosmos();
const pen = await Secp256k1Pen.fromMnemonic(faucet.mnemonic);
const client = CosmWasmClient.makeWritable(httpUrl, faucet.address, signBytes => pen.sign(signBytes));
const codeId = await client.upload(getRandomizedHackatom());
const transferAmount: readonly Coin[] = [
{
amount: "1234",
denom: "ucosm",
},
{
amount: "321",
denom: "ustake",
},
];
const beneficiaryAddress = makeRandomAddress();
const contractAddress = await client.instantiate(
codeId,
{
verifier: faucet.address,
beneficiary: beneficiaryAddress,
},
"Let's see",
transferAmount,
);
const rest = new RestClient(httpUrl);
const balance = (await rest.authAccounts(contractAddress)).result.value.coins;
expect(balance).toEqual(transferAmount);
});
it("can instantiate one code multiple times", async () => {
pendingWithoutCosmos();
const pen = await Secp256k1Pen.fromMnemonic(faucet.mnemonic);
const client = CosmWasmClient.makeWritable(httpUrl, faucet.address, signBytes => pen.sign(signBytes));
const codeId = await client.upload(getRandomizedHackatom());
const contractAddress1 = await client.instantiate(codeId, {
verifier: faucet.address,
beneficiary: makeRandomAddress(),
});
const contractAddress2 = await client.instantiate(codeId, {
verifier: faucet.address,
beneficiary: makeRandomAddress(),
});
expect(contractAddress1).not.toEqual(contractAddress2);
});
});
describe("execute", () => {
it("works", async () => {
pendingWithoutCosmos();
const pen = await Secp256k1Pen.fromMnemonic(faucet.mnemonic);
const client = CosmWasmClient.makeWritable(httpUrl, faucet.address, signBytes => pen.sign(signBytes));
const codeId = await client.upload(getRandomizedHackatom());
// instantiate
const transferAmount: readonly Coin[] = [
{
amount: "233444",
denom: "ucosm",
},
{
amount: "5454",
denom: "ustake",
},
];
const beneficiaryAddress = makeRandomAddress();
const contractAddress = await client.instantiate(
codeId,
{
verifier: faucet.address,
beneficiary: beneficiaryAddress,
},
undefined,
transferAmount,
);
// execute
const result = await client.execute(contractAddress, {}, undefined);
const [firstLog] = result.logs;
expect(firstLog.log).toEqual(`released funds to ${beneficiaryAddress}`);
// Verify token transfer from contract to beneficiary
const rest = new RestClient(httpUrl);
const beneficiaryBalance = (await rest.authAccounts(beneficiaryAddress)).result.value.coins;
expect(beneficiaryBalance).toEqual(transferAmount);
const contractBalance = (await rest.authAccounts(contractAddress)).result.value.coins;
expect(contractBalance).toEqual([]);
});
});
describe("sendTokens", () => {
it("works", async () => {
pendingWithoutCosmos();
const pen = await Secp256k1Pen.fromMnemonic(faucet.mnemonic);
const client = CosmWasmClient.makeWritable(httpUrl, faucet.address, signBytes => pen.sign(signBytes));
// instantiate
const transferAmount: readonly Coin[] = [
{
amount: "7890",
denom: "ucosm",
},
];
const beneficiaryAddress = makeRandomAddress();
// no tokens here
const before = await client.getAccount(beneficiaryAddress);
expect(before).toBeUndefined();
// send
const result = await client.sendTokens(beneficiaryAddress, transferAmount, "for dinner");
const [firstLog] = result.logs;
expect(firstLog).toBeTruthy();
// got tokens
const after = await client.getAccount(beneficiaryAddress);
assert(after);
expect(after.coins).toEqual(transferAmount);
});
});
describe("queryContractRaw", () => {
const configKey = toAscii("config");
const otherKey = toAscii("this_does_not_exist");
@ -458,7 +320,7 @@ describe("CosmWasmClient", () => {
if (cosmosEnabled()) {
pendingWithoutCosmos();
const pen = await Secp256k1Pen.fromMnemonic(faucet.mnemonic);
const client = CosmWasmClient.makeWritable(httpUrl, faucet.address, signBytes => pen.sign(signBytes));
const client = new SigningCosmWasmClient(httpUrl, faucet.address, signBytes => pen.sign(signBytes));
const codeId = await client.upload(getRandomizedHackatom());
const initMsg = { verifier: makeRandomAddress(), beneficiary: makeRandomAddress() };
const contractAddress = await client.instantiate(codeId, initMsg);
@ -470,7 +332,7 @@ describe("CosmWasmClient", () => {
pendingWithoutCosmos();
assert(contract);
const client = CosmWasmClient.makeReadOnly(httpUrl);
const client = new CosmWasmClient(httpUrl);
const raw = await client.queryContractRaw(contract.address, configKey);
assert(raw, "must get result");
expect(JSON.parse(fromUtf8(raw))).toEqual({
@ -484,7 +346,7 @@ describe("CosmWasmClient", () => {
pendingWithoutCosmos();
assert(contract);
const client = CosmWasmClient.makeReadOnly(httpUrl);
const client = new CosmWasmClient(httpUrl);
const raw = await client.queryContractRaw(contract.address, otherKey);
expect(raw).toBeNull();
});
@ -494,7 +356,7 @@ describe("CosmWasmClient", () => {
assert(contract);
const nonExistentAddress = makeRandomAddress();
const client = CosmWasmClient.makeReadOnly(httpUrl);
const client = new CosmWasmClient(httpUrl);
await client.queryContractRaw(nonExistentAddress, configKey).then(
() => fail("must not succeed"),
error => expect(error).toMatch(`No contract found at address "${nonExistentAddress}"`),
@ -509,7 +371,7 @@ describe("CosmWasmClient", () => {
if (cosmosEnabled()) {
pendingWithoutCosmos();
const pen = await Secp256k1Pen.fromMnemonic(faucet.mnemonic);
const client = CosmWasmClient.makeWritable(httpUrl, faucet.address, signBytes => pen.sign(signBytes));
const client = new SigningCosmWasmClient(httpUrl, faucet.address, signBytes => pen.sign(signBytes));
const codeId = await client.upload(getRandomizedHackatom());
const initMsg = { verifier: makeRandomAddress(), beneficiary: makeRandomAddress() };
const contractAddress = await client.instantiate(codeId, initMsg);
@ -521,7 +383,7 @@ describe("CosmWasmClient", () => {
pendingWithoutCosmos();
assert(contract);
const client = CosmWasmClient.makeReadOnly(httpUrl);
const client = new CosmWasmClient(httpUrl);
const verifier = await client.queryContractSmart(contract.address, { verifier: {} });
expect(fromAscii(verifier)).toEqual(contract.initMsg.verifier);
});
@ -530,7 +392,7 @@ describe("CosmWasmClient", () => {
pendingWithoutCosmos();
assert(contract);
const client = CosmWasmClient.makeReadOnly(httpUrl);
const client = new CosmWasmClient(httpUrl);
await client.queryContractSmart(contract.address, { broken: {} }).then(
() => fail("must not succeed"),
error => expect(error).toMatch(/Error parsing QueryMsg/i),
@ -541,7 +403,7 @@ describe("CosmWasmClient", () => {
pendingWithoutCosmos();
const nonExistentAddress = makeRandomAddress();
const client = CosmWasmClient.makeReadOnly(httpUrl);
const client = new CosmWasmClient(httpUrl);
await client.queryContractSmart(nonExistentAddress, { verifier: {} }).then(
() => fail("must not succeed"),
error => expect(error).toMatch(`No contract found at address "${nonExistentAddress}"`),

View File

@ -1,59 +1,9 @@
import { Sha256 } from "@iov/crypto";
import { Encoding } from "@iov/encoding";
import { makeSignBytes, marshalTx } from "./encoding";
import { findAttribute, Log, parseLogs } from "./logs";
import { Log, parseLogs } from "./logs";
import { BlockResponse, RestClient, TxsResponse } from "./restclient";
import {
Coin,
CosmosSdkAccount,
CosmosSdkTx,
MsgExecuteContract,
MsgInstantiateContract,
MsgSend,
MsgStoreCode,
StdFee,
StdSignature,
} from "./types";
export interface FeeTable {
readonly upload: StdFee;
readonly init: StdFee;
readonly exec: StdFee;
readonly send: StdFee;
}
function singleAmount(amount: number, denom: string): readonly Coin[] {
return [{ amount: amount.toString(), denom: denom }];
}
const defaultFees: FeeTable = {
upload: {
amount: singleAmount(25000, "ucosm"),
gas: "1000000", // one million
},
init: {
amount: singleAmount(12500, "ucosm"),
gas: "500000", // 500k
},
exec: {
amount: singleAmount(5000, "ucosm"),
gas: "200000", // 200k
},
send: {
amount: singleAmount(2000, "ucosm"),
gas: "80000", // 80k
},
};
export interface SigningCallback {
(signBytes: Uint8Array): Promise<StdSignature>;
}
export interface SigningData {
readonly senderAddress: string;
readonly signCallback: SigningCallback;
}
import { CosmosSdkAccount, CosmosSdkTx } from "./types";
export interface GetNonceResult {
readonly accountNumber: number;
@ -93,44 +43,11 @@ function isSearchBySentFromOrToQuery(query: SearchTxQuery): query is SearchBySen
return (query as SearchBySentFromOrToQuery).sentFromOrTo !== undefined;
}
export interface ExecuteResult {
readonly logs: readonly Log[];
}
export class CosmWasmClient {
public static makeReadOnly(url: string): CosmWasmClient {
return new CosmWasmClient(url, undefined, {});
}
protected readonly restClient: RestClient;
public static makeWritable(
url: string,
senderAddress: string,
signCallback: SigningCallback,
feeTable?: Partial<FeeTable>,
): CosmWasmClient {
return new CosmWasmClient(
url,
{
senderAddress: senderAddress,
signCallback: signCallback,
},
feeTable || {},
);
}
private readonly restClient: RestClient;
private readonly signingData: SigningData | undefined;
private readonly fees: FeeTable;
private get signCallback(): SigningCallback {
if (!this.signingData) throw new Error("Signing data not set in this client");
return this.signingData.signCallback;
}
private constructor(url: string, signingData: SigningData | undefined, customFees: Partial<FeeTable>) {
public constructor(url: string) {
this.restClient = new RestClient(url);
this.signingData = signingData;
this.fees = { ...defaultFees, ...customFees };
}
public async chainId(): Promise<string> {
@ -138,11 +55,6 @@ export class CosmWasmClient {
return response.node_info.network;
}
public get senderAddress(): string {
if (!this.signingData) throw new Error("Signing data not set in this client");
return this.signingData.senderAddress;
}
/**
* Returns a 32 byte upper-case hex transaction hash (typically used as the transaction ID)
*/
@ -160,7 +72,7 @@ export class CosmWasmClient {
*
* @param address returns data for this address. When unset, the client's sender adddress is used.
*/
public async getNonce(address?: string): Promise<GetNonceResult> {
public async getNonce(address: string): Promise<GetNonceResult> {
const account = await this.getAccount(address);
if (!account) {
throw new Error(
@ -173,8 +85,8 @@ export class CosmWasmClient {
};
}
public async getAccount(address?: string): Promise<CosmosSdkAccount | undefined> {
const account = await this.restClient.authAccounts(address || this.senderAddress);
public async getAccount(address: string): Promise<CosmosSdkAccount | undefined> {
const account = await this.restClient.authAccounts(address);
const value = account.result.value;
return value.address === "" ? undefined : value;
}
@ -230,136 +142,6 @@ export class CosmWasmClient {
};
}
/** Uploads code and returns a code ID */
public async upload(wasmCode: Uint8Array, memo = ""): Promise<number> {
const storeCodeMsg: MsgStoreCode = {
type: "wasm/store-code",
value: {
sender: this.senderAddress,
// eslint-disable-next-line @typescript-eslint/camelcase
wasm_byte_code: Encoding.toBase64(wasmCode),
source: "",
builder: "",
},
};
const fee = this.fees.upload;
const { accountNumber, sequence } = await this.getNonce();
const chainId = await this.chainId();
const signBytes = makeSignBytes([storeCodeMsg], fee, chainId, memo, accountNumber, sequence);
const signature = await this.signCallback(signBytes);
const signedTx = {
msg: [storeCodeMsg],
fee: fee,
memo: memo,
signatures: [signature],
};
const result = await this.postTx(marshalTx(signedTx));
const codeIdAttr = findAttribute(result.logs, "message", "code_id");
const codeId = Number.parseInt(codeIdAttr.value, 10);
return codeId;
}
public async instantiate(
codeId: number,
initMsg: object,
memo = "",
transferAmount?: readonly Coin[],
): Promise<string> {
const instantiateMsg: MsgInstantiateContract = {
type: "wasm/instantiate",
value: {
sender: this.senderAddress,
// eslint-disable-next-line @typescript-eslint/camelcase
code_id: codeId.toString(),
// eslint-disable-next-line @typescript-eslint/camelcase
init_msg: initMsg,
// eslint-disable-next-line @typescript-eslint/camelcase
init_funds: transferAmount || [],
},
};
const fee = this.fees.init;
const { accountNumber, sequence } = await this.getNonce();
const chainId = await this.chainId();
const signBytes = makeSignBytes([instantiateMsg], fee, chainId, memo, accountNumber, sequence);
const signature = await this.signCallback(signBytes);
const signedTx = {
msg: [instantiateMsg],
fee: fee,
memo: memo,
signatures: [signature],
};
const result = await this.postTx(marshalTx(signedTx));
const contractAddressAttr = findAttribute(result.logs, "message", "contract_address");
return contractAddressAttr.value;
}
public async execute(
contractAddress: string,
handleMsg: object,
memo = "",
transferAmount?: readonly Coin[],
): Promise<ExecuteResult> {
const executeMsg: MsgExecuteContract = {
type: "wasm/execute",
value: {
sender: this.senderAddress,
contract: contractAddress,
msg: handleMsg,
// eslint-disable-next-line @typescript-eslint/camelcase
sent_funds: transferAmount || [],
},
};
const fee = this.fees.exec;
const { accountNumber, sequence } = await this.getNonce();
const chainId = await this.chainId();
const signBytes = makeSignBytes([executeMsg], fee, chainId, memo, accountNumber, sequence);
const signature = await this.signCallback(signBytes);
const signedTx = {
msg: [executeMsg],
fee: fee,
memo: memo,
signatures: [signature],
};
const result = await this.postTx(marshalTx(signedTx));
return {
logs: result.logs,
};
}
public async sendTokens(
recipientAddress: string,
transferAmount: readonly Coin[],
memo = "",
): Promise<PostTxResult> {
const sendMsg: MsgSend = {
type: "cosmos-sdk/MsgSend",
value: {
// eslint-disable-next-line @typescript-eslint/camelcase
from_address: this.senderAddress,
// eslint-disable-next-line @typescript-eslint/camelcase
to_address: recipientAddress,
amount: transferAmount,
},
};
const fee = this.fees.send;
const { accountNumber, sequence } = await this.getNonce();
const chainId = await this.chainId();
const signBytes = makeSignBytes([sendMsg], fee, chainId, memo, accountNumber, sequence);
const signature = await this.signCallback(signBytes);
const signedTx = {
msg: [sendMsg],
fee: fee,
memo: memo,
signatures: [signature],
};
return this.postTx(marshalTx(signedTx));
}
/**
* Returns the data at the key if present (raw contract dependent storage data)
* or null if no data at this key.

View File

@ -8,15 +8,12 @@ export { makeSignBytes, marshalTx } from "./encoding";
export { RestClient, TxsResponse } from "./restclient";
export {
CosmWasmClient,
ExecuteResult,
GetNonceResult,
PostTxResult,
SearchByHeightQuery,
SearchByIdQuery,
SearchBySentFromOrToQuery,
SearchTxQuery,
SigningCallback,
SigningData,
} from "./cosmwasmclient";
export { makeCosmoshubPath, Pen, PrehashType, Secp256k1Pen } from "./pen";
export {
@ -27,3 +24,4 @@ export {
} from "./pubkey";
export { findSequenceForSignedTx } from "./sequence";
export { encodeSecp256k1Signature, decodeSignature } from "./signature";
export { SigningCallback, SigningCosmWasmClient, ExecuteResult } from "./signingcosmwasmclient";

View File

@ -0,0 +1,164 @@
import { assert } from "@iov/utils";
import { Secp256k1Pen } from "./pen";
import { RestClient } from "./restclient";
import { SigningCosmWasmClient } from "./signingcosmwasmclient";
import { getRandomizedHackatom, makeRandomAddress, pendingWithoutCosmos } from "./testutils.spec";
import { Coin } from "./types";
const httpUrl = "http://localhost:1317";
const faucet = {
mnemonic:
"economy stock theory fatal elder harbor betray wasp final emotion task crumble siren bottom lizard educate guess current outdoor pair theory focus wife stone",
pubkey: {
type: "tendermint/PubKeySecp256k1",
value: "A08EGB7ro1ORuFhjOnZcSgwYlpe0DSFjVNUIkNNQxwKQ",
},
address: "cosmos1pkptre7fdkl6gfrzlesjjvhxhlc3r4gmmk8rs6",
};
describe("SigningCosmWasmClient", () => {
describe("makeReadOnly", () => {
it("can be constructed", async () => {
const pen = await Secp256k1Pen.fromMnemonic(faucet.mnemonic);
const client = new SigningCosmWasmClient(httpUrl, faucet.address, signBytes => pen.sign(signBytes));
expect(client).toBeTruthy();
});
});
describe("upload", () => {
it("works", async () => {
pendingWithoutCosmos();
const pen = await Secp256k1Pen.fromMnemonic(faucet.mnemonic);
const client = new SigningCosmWasmClient(httpUrl, faucet.address, signBytes => pen.sign(signBytes));
const codeId = await client.upload(getRandomizedHackatom());
expect(codeId).toBeGreaterThanOrEqual(1);
});
});
describe("instantiate", () => {
it("works with transfer amount", async () => {
pendingWithoutCosmos();
const pen = await Secp256k1Pen.fromMnemonic(faucet.mnemonic);
const client = new SigningCosmWasmClient(httpUrl, faucet.address, signBytes => pen.sign(signBytes));
const codeId = await client.upload(getRandomizedHackatom());
const transferAmount: readonly Coin[] = [
{
amount: "1234",
denom: "ucosm",
},
{
amount: "321",
denom: "ustake",
},
];
const beneficiaryAddress = makeRandomAddress();
const contractAddress = await client.instantiate(
codeId,
{
verifier: faucet.address,
beneficiary: beneficiaryAddress,
},
"Let's see",
transferAmount,
);
const rest = new RestClient(httpUrl);
const balance = (await rest.authAccounts(contractAddress)).result.value.coins;
expect(balance).toEqual(transferAmount);
});
it("can instantiate one code multiple times", async () => {
pendingWithoutCosmos();
const pen = await Secp256k1Pen.fromMnemonic(faucet.mnemonic);
const client = new SigningCosmWasmClient(httpUrl, faucet.address, signBytes => pen.sign(signBytes));
const codeId = await client.upload(getRandomizedHackatom());
const contractAddress1 = await client.instantiate(codeId, {
verifier: faucet.address,
beneficiary: makeRandomAddress(),
});
const contractAddress2 = await client.instantiate(codeId, {
verifier: faucet.address,
beneficiary: makeRandomAddress(),
});
expect(contractAddress1).not.toEqual(contractAddress2);
});
});
describe("execute", () => {
it("works", async () => {
pendingWithoutCosmos();
const pen = await Secp256k1Pen.fromMnemonic(faucet.mnemonic);
const client = new SigningCosmWasmClient(httpUrl, faucet.address, signBytes => pen.sign(signBytes));
const codeId = await client.upload(getRandomizedHackatom());
// instantiate
const transferAmount: readonly Coin[] = [
{
amount: "233444",
denom: "ucosm",
},
{
amount: "5454",
denom: "ustake",
},
];
const beneficiaryAddress = makeRandomAddress();
const contractAddress = await client.instantiate(
codeId,
{
verifier: faucet.address,
beneficiary: beneficiaryAddress,
},
undefined,
transferAmount,
);
// execute
const result = await client.execute(contractAddress, {}, undefined);
const [firstLog] = result.logs;
expect(firstLog.log).toEqual(`released funds to ${beneficiaryAddress}`);
// Verify token transfer from contract to beneficiary
const rest = new RestClient(httpUrl);
const beneficiaryBalance = (await rest.authAccounts(beneficiaryAddress)).result.value.coins;
expect(beneficiaryBalance).toEqual(transferAmount);
const contractBalance = (await rest.authAccounts(contractAddress)).result.value.coins;
expect(contractBalance).toEqual([]);
});
});
describe("sendTokens", () => {
it("works", async () => {
pendingWithoutCosmos();
const pen = await Secp256k1Pen.fromMnemonic(faucet.mnemonic);
const client = new SigningCosmWasmClient(httpUrl, faucet.address, signBytes => pen.sign(signBytes));
// instantiate
const transferAmount: readonly Coin[] = [
{
amount: "7890",
denom: "ucosm",
},
];
const beneficiaryAddress = makeRandomAddress();
// no tokens here
const before = await client.getAccount(beneficiaryAddress);
expect(before).toBeUndefined();
// send
const result = await client.sendTokens(beneficiaryAddress, transferAmount, "for dinner");
const [firstLog] = result.logs;
expect(firstLog).toBeTruthy();
// got tokens
const after = await client.getAccount(beneficiaryAddress);
assert(after);
expect(after.coins).toEqual(transferAmount);
});
});
});

View File

@ -0,0 +1,210 @@
import { Encoding } from "@iov/encoding";
import { CosmWasmClient, GetNonceResult, PostTxResult } from "./cosmwasmclient";
import { makeSignBytes, marshalTx } from "./encoding";
import { findAttribute, Log } from "./logs";
import {
Coin,
CosmosSdkAccount,
MsgExecuteContract,
MsgInstantiateContract,
MsgSend,
MsgStoreCode,
StdFee,
StdSignature,
} from "./types";
export interface SigningCallback {
(signBytes: Uint8Array): Promise<StdSignature>;
}
export interface FeeTable {
readonly upload: StdFee;
readonly init: StdFee;
readonly exec: StdFee;
readonly send: StdFee;
}
function singleAmount(amount: number, denom: string): readonly Coin[] {
return [{ amount: amount.toString(), denom: denom }];
}
const defaultFees: FeeTable = {
upload: {
amount: singleAmount(25000, "ucosm"),
gas: "1000000", // one million
},
init: {
amount: singleAmount(12500, "ucosm"),
gas: "500000", // 500k
},
exec: {
amount: singleAmount(5000, "ucosm"),
gas: "200000", // 200k
},
send: {
amount: singleAmount(2000, "ucosm"),
gas: "80000", // 80k
},
};
export interface ExecuteResult {
readonly logs: readonly Log[];
}
export class SigningCosmWasmClient extends CosmWasmClient {
public readonly senderAddress: string;
private readonly signCallback: SigningCallback;
private readonly fees: FeeTable;
public constructor(
url: string,
senderAddress: string,
signCallback: SigningCallback,
customFees?: Partial<FeeTable>,
) {
super(url);
this.senderAddress = senderAddress;
this.signCallback = signCallback;
this.fees = { ...defaultFees, ...(customFees || {}) };
}
public async getNonce(address?: string): Promise<GetNonceResult> {
return super.getNonce(address || this.senderAddress);
}
public async getAccount(address?: string): Promise<CosmosSdkAccount | undefined> {
return super.getAccount(address || this.senderAddress);
}
/** Uploads code and returns a code ID */
public async upload(wasmCode: Uint8Array, memo = ""): Promise<number> {
const storeCodeMsg: MsgStoreCode = {
type: "wasm/store-code",
value: {
sender: this.senderAddress,
// eslint-disable-next-line @typescript-eslint/camelcase
wasm_byte_code: Encoding.toBase64(wasmCode),
source: "",
builder: "",
},
};
const fee = this.fees.upload;
const { accountNumber, sequence } = await this.getNonce();
const chainId = await this.chainId();
const signBytes = makeSignBytes([storeCodeMsg], fee, chainId, memo, accountNumber, sequence);
const signature = await this.signCallback(signBytes);
const signedTx = {
msg: [storeCodeMsg],
fee: fee,
memo: memo,
signatures: [signature],
};
const result = await this.postTx(marshalTx(signedTx));
const codeIdAttr = findAttribute(result.logs, "message", "code_id");
const codeId = Number.parseInt(codeIdAttr.value, 10);
return codeId;
}
public async instantiate(
codeId: number,
initMsg: object,
memo = "",
transferAmount?: readonly Coin[],
): Promise<string> {
const instantiateMsg: MsgInstantiateContract = {
type: "wasm/instantiate",
value: {
sender: this.senderAddress,
// eslint-disable-next-line @typescript-eslint/camelcase
code_id: codeId.toString(),
// eslint-disable-next-line @typescript-eslint/camelcase
init_msg: initMsg,
// eslint-disable-next-line @typescript-eslint/camelcase
init_funds: transferAmount || [],
},
};
const fee = this.fees.init;
const { accountNumber, sequence } = await this.getNonce();
const chainId = await this.chainId();
const signBytes = makeSignBytes([instantiateMsg], fee, chainId, memo, accountNumber, sequence);
const signature = await this.signCallback(signBytes);
const signedTx = {
msg: [instantiateMsg],
fee: fee,
memo: memo,
signatures: [signature],
};
const result = await this.postTx(marshalTx(signedTx));
const contractAddressAttr = findAttribute(result.logs, "message", "contract_address");
return contractAddressAttr.value;
}
public async execute(
contractAddress: string,
handleMsg: object,
memo = "",
transferAmount?: readonly Coin[],
): Promise<ExecuteResult> {
const executeMsg: MsgExecuteContract = {
type: "wasm/execute",
value: {
sender: this.senderAddress,
contract: contractAddress,
msg: handleMsg,
// eslint-disable-next-line @typescript-eslint/camelcase
sent_funds: transferAmount || [],
},
};
const fee = this.fees.exec;
const { accountNumber, sequence } = await this.getNonce();
const chainId = await this.chainId();
const signBytes = makeSignBytes([executeMsg], fee, chainId, memo, accountNumber, sequence);
const signature = await this.signCallback(signBytes);
const signedTx = {
msg: [executeMsg],
fee: fee,
memo: memo,
signatures: [signature],
};
const result = await this.postTx(marshalTx(signedTx));
return {
logs: result.logs,
};
}
public async sendTokens(
recipientAddress: string,
transferAmount: readonly Coin[],
memo = "",
): Promise<PostTxResult> {
const sendMsg: MsgSend = {
type: "cosmos-sdk/MsgSend",
value: {
// eslint-disable-next-line @typescript-eslint/camelcase
from_address: this.senderAddress,
// eslint-disable-next-line @typescript-eslint/camelcase
to_address: recipientAddress,
amount: transferAmount,
},
};
const fee = this.fees.send;
const { accountNumber, sequence } = await this.getNonce();
const chainId = await this.chainId();
const signBytes = makeSignBytes([sendMsg], fee, chainId, memo, accountNumber, sequence);
const signature = await this.signCallback(signBytes);
const signedTx = {
msg: [sendMsg],
fee: fee,
memo: memo,
signatures: [signature],
};
return this.postTx(marshalTx(signedTx));
}
}

View File

@ -58,6 +58,16 @@ export const tendermintIdMatcher = /^[0-9A-F]{64}$/;
export const tendermintOptionalIdMatcher = /^([0-9A-F]{64}|)$/;
export const tendermintAddressMatcher = /^[0-9A-F]{40}$/;
export function cosmosEnabled(): boolean {
return !!process.env.COSMOS_ENABLED;
}
export function pendingWithoutCosmos(): void {
if (!cosmosEnabled()) {
return pending("Set COSMOS_ENABLED to enable Cosmos node-based tests");
}
}
describe("leb128", () => {
describe("leb128Encode", () => {
it("works for single byte values", () => {

View File

@ -1,19 +1,6 @@
import { Log } from "./logs";
import { BlockResponse, TxsResponse } from "./restclient";
import { Coin, CosmosSdkAccount, CosmosSdkTx, StdFee, StdSignature } from "./types";
export interface FeeTable {
readonly upload: StdFee;
readonly init: StdFee;
readonly exec: StdFee;
readonly send: StdFee;
}
export interface SigningCallback {
(signBytes: Uint8Array): Promise<StdSignature>;
}
export interface SigningData {
readonly senderAddress: string;
readonly signCallback: SigningCallback;
}
import { BlockResponse, RestClient, TxsResponse } from "./restclient";
import { CosmosSdkAccount, CosmosSdkTx } from "./types";
export interface GetNonceResult {
readonly accountNumber: number;
readonly sequence: number;
@ -34,24 +21,10 @@ export interface SearchBySentFromOrToQuery {
readonly sentFromOrTo: string;
}
export declare type SearchTxQuery = SearchByIdQuery | SearchByHeightQuery | SearchBySentFromOrToQuery;
export interface ExecuteResult {
readonly logs: readonly Log[];
}
export declare class CosmWasmClient {
static makeReadOnly(url: string): CosmWasmClient;
static makeWritable(
url: string,
senderAddress: string,
signCallback: SigningCallback,
feeTable?: Partial<FeeTable>,
): CosmWasmClient;
private readonly restClient;
private readonly signingData;
private readonly fees;
private get signCallback();
private constructor();
protected readonly restClient: RestClient;
constructor(url: string);
chainId(): Promise<string>;
get senderAddress(): string;
/**
* Returns a 32 byte upper-case hex transaction hash (typically used as the transaction ID)
*/
@ -63,8 +36,8 @@ 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>;
getNonce(address: string): Promise<GetNonceResult>;
getAccount(address: string): Promise<CosmosSdkAccount | undefined>;
/**
* Gets block header and meta
*
@ -73,21 +46,6 @@ export declare class CosmWasmClient {
getBlock(height?: number): Promise<BlockResponse>;
searchTx(query: SearchTxQuery): Promise<readonly TxsResponse[]>;
postTx(tx: Uint8Array): Promise<PostTxResult>;
/** Uploads code and returns a code ID */
upload(wasmCode: Uint8Array, memo?: string): Promise<number>;
instantiate(
codeId: number,
initMsg: object,
memo?: string,
transferAmount?: readonly Coin[],
): Promise<string>;
execute(
contractAddress: string,
handleMsg: object,
memo?: string,
transferAmount?: readonly Coin[],
): Promise<ExecuteResult>;
sendTokens(recipientAddress: string, transferAmount: readonly Coin[], memo?: string): Promise<PostTxResult>;
/**
* Returns the data at the key if present (raw contract dependent storage data)
* or null if no data at this key.

View File

@ -7,15 +7,12 @@ export { makeSignBytes, marshalTx } from "./encoding";
export { RestClient, TxsResponse } from "./restclient";
export {
CosmWasmClient,
ExecuteResult,
GetNonceResult,
PostTxResult,
SearchByHeightQuery,
SearchByIdQuery,
SearchBySentFromOrToQuery,
SearchTxQuery,
SigningCallback,
SigningData,
} from "./cosmwasmclient";
export { makeCosmoshubPath, Pen, PrehashType, Secp256k1Pen } from "./pen";
export {
@ -26,3 +23,4 @@ export {
} from "./pubkey";
export { findSequenceForSignedTx } from "./sequence";
export { encodeSecp256k1Signature, decodeSignature } from "./signature";
export { SigningCallback, SigningCosmWasmClient, ExecuteResult } from "./signingcosmwasmclient";

View File

@ -0,0 +1,43 @@
import { CosmWasmClient, GetNonceResult, PostTxResult } from "./cosmwasmclient";
import { Log } from "./logs";
import { Coin, CosmosSdkAccount, StdFee, StdSignature } from "./types";
export interface SigningCallback {
(signBytes: Uint8Array): Promise<StdSignature>;
}
export interface FeeTable {
readonly upload: StdFee;
readonly init: StdFee;
readonly exec: StdFee;
readonly send: StdFee;
}
export interface ExecuteResult {
readonly logs: readonly Log[];
}
export declare class SigningCosmWasmClient extends CosmWasmClient {
readonly senderAddress: string;
private readonly signCallback;
private readonly fees;
constructor(
url: string,
senderAddress: string,
signCallback: SigningCallback,
customFees?: Partial<FeeTable>,
);
getNonce(address?: string): Promise<GetNonceResult>;
getAccount(address?: string): Promise<CosmosSdkAccount | undefined>;
/** Uploads code and returns a code ID */
upload(wasmCode: Uint8Array, memo?: string): Promise<number>;
instantiate(
codeId: number,
initMsg: object,
memo?: string,
transferAmount?: readonly Coin[],
): Promise<string>;
execute(
contractAddress: string,
handleMsg: object,
memo?: string,
transferAmount?: readonly Coin[],
): Promise<ExecuteResult>;
sendTokens(recipientAddress: string, transferAmount: readonly Coin[], memo?: string): Promise<PostTxResult>;
}