Create package @cosmwasm/sdk38
This commit is contained in:
parent
935b18b921
commit
b1003be1df
1
packages/sdk38/.eslintignore
Symbolic link
1
packages/sdk38/.eslintignore
Symbolic link
@ -0,0 +1 @@
|
||||
../../.eslintignore
|
||||
3
packages/sdk38/.gitignore
vendored
Normal file
3
packages/sdk38/.gitignore
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
build/
|
||||
dist/
|
||||
docs/
|
||||
12
packages/sdk38/README.md
Normal file
12
packages/sdk38/README.md
Normal file
@ -0,0 +1,12 @@
|
||||
# @cosmwasm/sdk38
|
||||
|
||||
[](https://www.npmjs.com/package/@cosmwasm/sdk38)
|
||||
|
||||
A client library for the Cosmos SDK 0.38.
|
||||
|
||||
## License
|
||||
|
||||
This package is part of the cosmwasm-js repository, licensed under the Apache
|
||||
License 2.0 (see
|
||||
[NOTICE](https://github.com/confio/cosmwasm-js/blob/master/NOTICE) and
|
||||
[LICENSE](https://github.com/confio/cosmwasm-js/blob/master/LICENSE)).
|
||||
26
packages/sdk38/jasmine-testrunner.js
Executable file
26
packages/sdk38/jasmine-testrunner.js
Executable file
@ -0,0 +1,26 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
require("source-map-support").install();
|
||||
const defaultSpecReporterConfig = require("../../jasmine-spec-reporter.config.json");
|
||||
|
||||
// setup Jasmine
|
||||
const Jasmine = require("jasmine");
|
||||
const jasmine = new Jasmine();
|
||||
jasmine.loadConfig({
|
||||
spec_dir: "build",
|
||||
spec_files: ["**/*.spec.js"],
|
||||
helpers: [],
|
||||
random: false,
|
||||
seed: null,
|
||||
stopSpecOnExpectationFailure: false,
|
||||
});
|
||||
jasmine.jasmine.DEFAULT_TIMEOUT_INTERVAL = 15 * 1000;
|
||||
|
||||
// setup reporter
|
||||
const { SpecReporter } = require("jasmine-spec-reporter");
|
||||
const reporter = new SpecReporter({ ...defaultSpecReporterConfig });
|
||||
|
||||
// initialize and execute
|
||||
jasmine.env.clearReporters();
|
||||
jasmine.addReporter(reporter);
|
||||
jasmine.execute();
|
||||
54
packages/sdk38/karma.conf.js
Normal file
54
packages/sdk38/karma.conf.js
Normal file
@ -0,0 +1,54 @@
|
||||
module.exports = function (config) {
|
||||
config.set({
|
||||
// base path that will be used to resolve all patterns (eg. files, exclude)
|
||||
basePath: ".",
|
||||
|
||||
// frameworks to use
|
||||
// available frameworks: https://npmjs.org/browse/keyword/karma-adapter
|
||||
frameworks: ["jasmine"],
|
||||
|
||||
// list of files / patterns to load in the browser
|
||||
files: ["dist/web/tests.js"],
|
||||
|
||||
client: {
|
||||
jasmine: {
|
||||
random: false,
|
||||
timeoutInterval: 15000,
|
||||
},
|
||||
},
|
||||
|
||||
// test results reporter to use
|
||||
// possible values: 'dots', 'progress'
|
||||
// available reporters: https://npmjs.org/browse/keyword/karma-reporter
|
||||
reporters: ["progress", "kjhtml"],
|
||||
|
||||
// web server port
|
||||
port: 9876,
|
||||
|
||||
// enable / disable colors in the output (reporters and logs)
|
||||
colors: true,
|
||||
|
||||
// level of logging
|
||||
// possible values: config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG
|
||||
logLevel: config.LOG_INFO,
|
||||
|
||||
// enable / disable watching file and executing tests whenever any file changes
|
||||
autoWatch: false,
|
||||
|
||||
// start these browsers
|
||||
// available browser launchers: https://npmjs.org/browse/keyword/karma-launcher
|
||||
browsers: ["Firefox"],
|
||||
|
||||
browserNoActivityTimeout: 90000,
|
||||
|
||||
// Keep brower open for debugging. This is overridden by yarn scripts
|
||||
singleRun: false,
|
||||
|
||||
customLaunchers: {
|
||||
ChromeHeadlessInsecure: {
|
||||
base: "ChromeHeadless",
|
||||
flags: ["--disable-web-security"],
|
||||
},
|
||||
},
|
||||
});
|
||||
};
|
||||
1
packages/sdk38/nonces/README.txt
Normal file
1
packages/sdk38/nonces/README.txt
Normal file
@ -0,0 +1 @@
|
||||
Directory used to trigger lerna package updates for all packages
|
||||
48
packages/sdk38/package.json
Normal file
48
packages/sdk38/package.json
Normal file
@ -0,0 +1,48 @@
|
||||
{
|
||||
"name": "@cosmwasm/sdk38",
|
||||
"version": "0.8.0",
|
||||
"description": "Utilities for Cosmos SDK 0.38",
|
||||
"author": "Ethan Frey <ethanfrey@users.noreply.github.com>",
|
||||
"license": "Apache-2.0",
|
||||
"main": "build/index.js",
|
||||
"types": "types/index.d.ts",
|
||||
"files": [
|
||||
"build/",
|
||||
"types/",
|
||||
"*.md",
|
||||
"!*.spec.*",
|
||||
"!**/testdata/"
|
||||
],
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/CosmWasm/cosmwasm-js/tree/master/packages/sdk38"
|
||||
},
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
"scripts": {
|
||||
"docs": "shx rm -rf docs && typedoc --options typedoc.js",
|
||||
"format": "prettier --write --loglevel warn \"./src/**/*.ts\"",
|
||||
"lint": "eslint --max-warnings 0 \"**/*.{js,ts}\"",
|
||||
"lint-fix": "eslint --max-warnings 0 \"**/*.{js,ts}\" --fix",
|
||||
"move-types": "shx rm -rf ./types/* && shx mv build/types/* ./types && rm -rf ./types/testdata && shx rm -f ./types/*.spec.d.ts",
|
||||
"format-types": "prettier --write --loglevel warn \"./types/**/*.d.ts\"",
|
||||
"build": "shx rm -rf ./build && tsc && yarn move-types && yarn format-types",
|
||||
"build-or-skip": "[ -n \"$SKIP_BUILD\" ] || yarn build",
|
||||
"test-node": "node jasmine-testrunner.js",
|
||||
"test-firefox": "yarn pack-web && karma start --single-run --browsers Firefox",
|
||||
"test-chrome": "yarn pack-web && karma start --single-run --browsers ChromeHeadlessInsecure",
|
||||
"test": "yarn build-or-skip && yarn test-node",
|
||||
"pack-web": "yarn build-or-skip && webpack --mode development --config webpack.web.config.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"@iov/crypto": "^2.1.0",
|
||||
"@iov/encoding": "^2.1.0",
|
||||
"@iov/utils": "^2.0.2",
|
||||
"axios": "^0.19.0",
|
||||
"fast-deep-equal": "^3.1.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"readonly-date": "^1.0.0"
|
||||
}
|
||||
}
|
||||
27
packages/sdk38/src/address.spec.ts
Normal file
27
packages/sdk38/src/address.spec.ts
Normal file
@ -0,0 +1,27 @@
|
||||
import { Encoding } from "@iov/encoding";
|
||||
|
||||
import { pubkeyToAddress } from "./address";
|
||||
|
||||
const { toBase64, fromHex } = Encoding;
|
||||
|
||||
describe("address", () => {
|
||||
describe("pubkeyToAddress", () => {
|
||||
it("works for Secp256k1 compressed", () => {
|
||||
const prefix = "cosmos";
|
||||
const pubkey = {
|
||||
type: "tendermint/PubKeySecp256k1",
|
||||
value: "AtQaCqFnshaZQp6rIkvAPyzThvCvXSDO+9AzbxVErqJP",
|
||||
};
|
||||
expect(pubkeyToAddress(pubkey, prefix)).toEqual("cosmos1h806c7khnvmjlywdrkdgk2vrayy2mmvf9rxk2r");
|
||||
});
|
||||
|
||||
it("works for Ed25519", () => {
|
||||
const prefix = "cosmos";
|
||||
const pubkey = {
|
||||
type: "tendermint/PubKeyEd25519",
|
||||
value: toBase64(fromHex("12ee6f581fe55673a1e9e1382a0829e32075a0aa4763c968bc526e1852e78c95")),
|
||||
};
|
||||
expect(pubkeyToAddress(pubkey, prefix)).toEqual("cosmos1pfq05em6sfkls66ut4m2257p7qwlk448h8mysz");
|
||||
});
|
||||
});
|
||||
});
|
||||
42
packages/sdk38/src/address.ts
Normal file
42
packages/sdk38/src/address.ts
Normal file
@ -0,0 +1,42 @@
|
||||
import { Ripemd160, Sha256 } from "@iov/crypto";
|
||||
import { Bech32, Encoding } from "@iov/encoding";
|
||||
|
||||
import { PubKey, pubkeyType } from "./types";
|
||||
|
||||
const { fromBase64 } = Encoding;
|
||||
|
||||
export function rawSecp256k1PubkeyToAddress(pubkeyRaw: Uint8Array, prefix: string): string {
|
||||
if (pubkeyRaw.length !== 33) {
|
||||
throw new Error(`Invalid Secp256k1 pubkey length (compressed): ${pubkeyRaw.length}`);
|
||||
}
|
||||
const hash1 = new Sha256(pubkeyRaw).digest();
|
||||
const hash2 = new Ripemd160(hash1).digest();
|
||||
return Bech32.encode(prefix, hash2);
|
||||
}
|
||||
|
||||
// See https://github.com/tendermint/tendermint/blob/f2ada0a604b4c0763bda2f64fac53d506d3beca7/docs/spec/blockchain/encoding.md#public-key-cryptography
|
||||
// This assumes we already have a cosmos-compressed pubkey
|
||||
export function pubkeyToAddress(pubkey: PubKey, prefix: string): string {
|
||||
const pubkeyBytes = fromBase64(pubkey.value);
|
||||
switch (pubkey.type) {
|
||||
case pubkeyType.secp256k1: {
|
||||
return rawSecp256k1PubkeyToAddress(pubkeyBytes, prefix);
|
||||
}
|
||||
case pubkeyType.ed25519: {
|
||||
if (pubkeyBytes.length !== 32) {
|
||||
throw new Error(`Invalid Ed25519 pubkey length: ${pubkeyBytes.length}`);
|
||||
}
|
||||
const hash = new Sha256(pubkeyBytes).digest();
|
||||
return Bech32.encode(prefix, hash.slice(0, 20));
|
||||
}
|
||||
case pubkeyType.sr25519: {
|
||||
if (pubkeyBytes.length !== 32) {
|
||||
throw new Error(`Invalid Sr25519 pubkey length: ${pubkeyBytes.length}`);
|
||||
}
|
||||
const hash = new Sha256(pubkeyBytes).digest();
|
||||
return Bech32.encode(prefix, hash.slice(0, 20));
|
||||
}
|
||||
default:
|
||||
throw new Error("Unrecognized public key algorithm");
|
||||
}
|
||||
}
|
||||
14
packages/sdk38/src/coins.ts
Normal file
14
packages/sdk38/src/coins.ts
Normal file
@ -0,0 +1,14 @@
|
||||
export interface Coin {
|
||||
readonly denom: string;
|
||||
readonly amount: string;
|
||||
}
|
||||
|
||||
/** Creates a coin */
|
||||
export function coin(amount: number, denom: string): Coin {
|
||||
return { amount: amount.toString(), denom: denom };
|
||||
}
|
||||
|
||||
/** Creates a list of coins with one element */
|
||||
export function coins(amount: number, denom: string): Coin[] {
|
||||
return [coin(amount, denom)];
|
||||
}
|
||||
366
packages/sdk38/src/cosmosclient.searchtx.spec.ts
Normal file
366
packages/sdk38/src/cosmosclient.searchtx.spec.ts
Normal file
@ -0,0 +1,366 @@
|
||||
/* eslint-disable @typescript-eslint/camelcase */
|
||||
import { assert, sleep } from "@iov/utils";
|
||||
|
||||
import { Coin } from "./coins";
|
||||
import { CosmosClient } from "./cosmosclient";
|
||||
import { makeSignBytes } from "./encoding";
|
||||
import { Secp256k1Pen } from "./pen";
|
||||
import { RestClient } from "./restclient";
|
||||
import { SigningCosmosClient } from "./signingcosmosclient";
|
||||
import {
|
||||
faucet,
|
||||
fromOneElementArray,
|
||||
makeRandomAddress,
|
||||
pendingWithoutWasmd,
|
||||
wasmd,
|
||||
wasmdEnabled,
|
||||
} from "./testutils.spec";
|
||||
import { CosmosSdkTx, isMsgSend, MsgSend } from "./types";
|
||||
|
||||
describe("CosmosClient.searchTx", () => {
|
||||
let sendSuccessful:
|
||||
| {
|
||||
readonly sender: string;
|
||||
readonly recipient: string;
|
||||
readonly hash: string;
|
||||
readonly height: number;
|
||||
readonly tx: CosmosSdkTx;
|
||||
}
|
||||
| undefined;
|
||||
let sendUnsuccessful:
|
||||
| {
|
||||
readonly sender: string;
|
||||
readonly recipient: string;
|
||||
readonly hash: string;
|
||||
readonly height: number;
|
||||
readonly tx: CosmosSdkTx;
|
||||
}
|
||||
| undefined;
|
||||
|
||||
beforeAll(async () => {
|
||||
if (wasmdEnabled()) {
|
||||
const pen = await Secp256k1Pen.fromMnemonic(faucet.mnemonic);
|
||||
const client = new SigningCosmosClient(wasmd.endpoint, faucet.address, (signBytes) =>
|
||||
pen.sign(signBytes),
|
||||
);
|
||||
|
||||
{
|
||||
const recipient = makeRandomAddress();
|
||||
const transferAmount: Coin = {
|
||||
denom: "ucosm",
|
||||
amount: "1234567",
|
||||
};
|
||||
const result = await client.sendTokens(recipient, [transferAmount]);
|
||||
await sleep(50); // wait until tx is indexed
|
||||
const txDetails = await new RestClient(wasmd.endpoint).txById(result.transactionHash);
|
||||
sendSuccessful = {
|
||||
sender: faucet.address,
|
||||
recipient: recipient,
|
||||
hash: result.transactionHash,
|
||||
height: Number.parseInt(txDetails.height, 10),
|
||||
tx: txDetails.tx,
|
||||
};
|
||||
}
|
||||
|
||||
{
|
||||
const memo = "Sending more than I can afford";
|
||||
const recipient = makeRandomAddress();
|
||||
const transferAmount = [
|
||||
{
|
||||
denom: "ucosm",
|
||||
amount: "123456700000000",
|
||||
},
|
||||
];
|
||||
const sendMsg: MsgSend = {
|
||||
type: "cosmos-sdk/MsgSend",
|
||||
value: {
|
||||
// eslint-disable-next-line @typescript-eslint/camelcase
|
||||
from_address: faucet.address,
|
||||
// eslint-disable-next-line @typescript-eslint/camelcase
|
||||
to_address: recipient,
|
||||
amount: transferAmount,
|
||||
},
|
||||
};
|
||||
const fee = {
|
||||
amount: [
|
||||
{
|
||||
denom: "ucosm",
|
||||
amount: "2000",
|
||||
},
|
||||
],
|
||||
gas: "80000", // 80k
|
||||
};
|
||||
const { accountNumber, sequence } = await client.getNonce();
|
||||
const chainId = await client.getChainId();
|
||||
const signBytes = makeSignBytes([sendMsg], fee, chainId, memo, accountNumber, sequence);
|
||||
const signature = await pen.sign(signBytes);
|
||||
const tx: CosmosSdkTx = {
|
||||
type: "cosmos-sdk/StdTx",
|
||||
value: {
|
||||
msg: [sendMsg],
|
||||
fee: fee,
|
||||
memo: memo,
|
||||
signatures: [signature],
|
||||
},
|
||||
};
|
||||
const transactionId = await client.getIdentifier(tx);
|
||||
const heightBeforeThis = await client.getHeight();
|
||||
try {
|
||||
await client.postTx(tx.value);
|
||||
} catch (error) {
|
||||
// postTx() throws on execution failures, which is a questionable design. Ignore for now.
|
||||
// console.log(error);
|
||||
}
|
||||
sendUnsuccessful = {
|
||||
sender: faucet.address,
|
||||
recipient: recipient,
|
||||
hash: transactionId,
|
||||
height: heightBeforeThis + 1,
|
||||
tx: tx,
|
||||
};
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
describe("with SearchByIdQuery", () => {
|
||||
it("can search successful tx by ID", async () => {
|
||||
pendingWithoutWasmd();
|
||||
assert(sendSuccessful, "value must be set in beforeAll()");
|
||||
const client = new CosmosClient(wasmd.endpoint);
|
||||
const result = await client.searchTx({ id: sendSuccessful.hash });
|
||||
expect(result.length).toEqual(1);
|
||||
expect(result[0]).toEqual(
|
||||
jasmine.objectContaining({
|
||||
height: sendSuccessful.height,
|
||||
hash: sendSuccessful.hash,
|
||||
code: 0,
|
||||
tx: sendSuccessful.tx,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("can search unsuccessful tx by ID", async () => {
|
||||
pendingWithoutWasmd();
|
||||
assert(sendUnsuccessful, "value must be set in beforeAll()");
|
||||
const client = new CosmosClient(wasmd.endpoint);
|
||||
const result = await client.searchTx({ id: sendUnsuccessful.hash });
|
||||
expect(result.length).toEqual(1);
|
||||
expect(result[0]).toEqual(
|
||||
jasmine.objectContaining({
|
||||
height: sendUnsuccessful.height,
|
||||
hash: sendUnsuccessful.hash,
|
||||
code: 5,
|
||||
tx: sendUnsuccessful.tx,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("can search by ID (non existent)", async () => {
|
||||
pendingWithoutWasmd();
|
||||
const client = new CosmosClient(wasmd.endpoint);
|
||||
const nonExistentId = "0000000000000000000000000000000000000000000000000000000000000000";
|
||||
const result = await client.searchTx({ id: nonExistentId });
|
||||
expect(result.length).toEqual(0);
|
||||
});
|
||||
|
||||
it("can search by ID and filter by minHeight", async () => {
|
||||
pendingWithoutWasmd();
|
||||
assert(sendSuccessful);
|
||||
const client = new CosmosClient(wasmd.endpoint);
|
||||
const query = { id: sendSuccessful.hash };
|
||||
|
||||
{
|
||||
const result = await client.searchTx(query, { minHeight: 0 });
|
||||
expect(result.length).toEqual(1);
|
||||
}
|
||||
|
||||
{
|
||||
const result = await client.searchTx(query, { minHeight: sendSuccessful.height - 1 });
|
||||
expect(result.length).toEqual(1);
|
||||
}
|
||||
|
||||
{
|
||||
const result = await client.searchTx(query, { minHeight: sendSuccessful.height });
|
||||
expect(result.length).toEqual(1);
|
||||
}
|
||||
|
||||
{
|
||||
const result = await client.searchTx(query, { minHeight: sendSuccessful.height + 1 });
|
||||
expect(result.length).toEqual(0);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe("with SearchByHeightQuery", () => {
|
||||
it("can search successful tx by height", async () => {
|
||||
pendingWithoutWasmd();
|
||||
assert(sendSuccessful, "value must be set in beforeAll()");
|
||||
const client = new CosmosClient(wasmd.endpoint);
|
||||
const result = await client.searchTx({ height: sendSuccessful.height });
|
||||
expect(result.length).toEqual(1);
|
||||
expect(result[0]).toEqual(
|
||||
jasmine.objectContaining({
|
||||
height: sendSuccessful.height,
|
||||
hash: sendSuccessful.hash,
|
||||
code: 0,
|
||||
tx: sendSuccessful.tx,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("can search unsuccessful tx by height", async () => {
|
||||
pendingWithoutWasmd();
|
||||
assert(sendUnsuccessful, "value must be set in beforeAll()");
|
||||
const client = new CosmosClient(wasmd.endpoint);
|
||||
const result = await client.searchTx({ height: sendUnsuccessful.height });
|
||||
expect(result.length).toEqual(1);
|
||||
expect(result[0]).toEqual(
|
||||
jasmine.objectContaining({
|
||||
height: sendUnsuccessful.height,
|
||||
hash: sendUnsuccessful.hash,
|
||||
code: 5,
|
||||
tx: sendUnsuccessful.tx,
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("with SearchBySentFromOrToQuery", () => {
|
||||
it("can search by sender", async () => {
|
||||
pendingWithoutWasmd();
|
||||
assert(sendSuccessful, "value must be set in beforeAll()");
|
||||
const client = new CosmosClient(wasmd.endpoint);
|
||||
const results = await client.searchTx({ sentFromOrTo: sendSuccessful.sender });
|
||||
expect(results.length).toBeGreaterThanOrEqual(1);
|
||||
|
||||
// Check basic structure of all results
|
||||
for (const result of results) {
|
||||
const containsMsgWithSender = !!result.tx.value.msg.find(
|
||||
(msg) => isMsgSend(msg) && msg.value.from_address == sendSuccessful!.sender,
|
||||
);
|
||||
const containsMsgWithRecipient = !!result.tx.value.msg.find(
|
||||
(msg) => isMsgSend(msg) && msg.value.to_address === sendSuccessful!.sender,
|
||||
);
|
||||
expect(containsMsgWithSender || containsMsgWithRecipient).toEqual(true);
|
||||
}
|
||||
|
||||
// Check details of most recent result
|
||||
expect(results[results.length - 1]).toEqual(
|
||||
jasmine.objectContaining({
|
||||
height: sendSuccessful.height,
|
||||
hash: sendSuccessful.hash,
|
||||
tx: sendSuccessful.tx,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("can search by recipient", async () => {
|
||||
pendingWithoutWasmd();
|
||||
assert(sendSuccessful, "value must be set in beforeAll()");
|
||||
const client = new CosmosClient(wasmd.endpoint);
|
||||
const results = await client.searchTx({ sentFromOrTo: sendSuccessful.recipient });
|
||||
expect(results.length).toBeGreaterThanOrEqual(1);
|
||||
|
||||
// Check basic structure of all results
|
||||
for (const result of results) {
|
||||
const msg = fromOneElementArray(result.tx.value.msg);
|
||||
assert(isMsgSend(msg), `${result.hash} (height ${result.height}) is not a bank send transaction`);
|
||||
expect(
|
||||
msg.value.to_address === sendSuccessful.recipient ||
|
||||
msg.value.from_address == sendSuccessful.recipient,
|
||||
).toEqual(true);
|
||||
}
|
||||
|
||||
// Check details of most recent result
|
||||
expect(results[results.length - 1]).toEqual(
|
||||
jasmine.objectContaining({
|
||||
height: sendSuccessful.height,
|
||||
hash: sendSuccessful.hash,
|
||||
tx: sendSuccessful.tx,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("can search by recipient and filter by minHeight", async () => {
|
||||
pendingWithoutWasmd();
|
||||
assert(sendSuccessful);
|
||||
const client = new CosmosClient(wasmd.endpoint);
|
||||
const query = { sentFromOrTo: sendSuccessful.recipient };
|
||||
|
||||
{
|
||||
const result = await client.searchTx(query, { minHeight: 0 });
|
||||
expect(result.length).toEqual(1);
|
||||
}
|
||||
|
||||
{
|
||||
const result = await client.searchTx(query, { minHeight: sendSuccessful.height - 1 });
|
||||
expect(result.length).toEqual(1);
|
||||
}
|
||||
|
||||
{
|
||||
const result = await client.searchTx(query, { minHeight: sendSuccessful.height });
|
||||
expect(result.length).toEqual(1);
|
||||
}
|
||||
|
||||
{
|
||||
const result = await client.searchTx(query, { minHeight: sendSuccessful.height + 1 });
|
||||
expect(result.length).toEqual(0);
|
||||
}
|
||||
});
|
||||
|
||||
it("can search by recipient and filter by maxHeight", async () => {
|
||||
pendingWithoutWasmd();
|
||||
assert(sendSuccessful);
|
||||
const client = new CosmosClient(wasmd.endpoint);
|
||||
const query = { sentFromOrTo: sendSuccessful.recipient };
|
||||
|
||||
{
|
||||
const result = await client.searchTx(query, { maxHeight: 9999999999999 });
|
||||
expect(result.length).toEqual(1);
|
||||
}
|
||||
|
||||
{
|
||||
const result = await client.searchTx(query, { maxHeight: sendSuccessful.height + 1 });
|
||||
expect(result.length).toEqual(1);
|
||||
}
|
||||
|
||||
{
|
||||
const result = await client.searchTx(query, { maxHeight: sendSuccessful.height });
|
||||
expect(result.length).toEqual(1);
|
||||
}
|
||||
|
||||
{
|
||||
const result = await client.searchTx(query, { maxHeight: sendSuccessful.height - 1 });
|
||||
expect(result.length).toEqual(0);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe("with SearchByTagsQuery", () => {
|
||||
it("can search by transfer.recipient", async () => {
|
||||
pendingWithoutWasmd();
|
||||
assert(sendSuccessful, "value must be set in beforeAll()");
|
||||
const client = new CosmosClient(wasmd.endpoint);
|
||||
const results = await client.searchTx({
|
||||
tags: [{ key: "transfer.recipient", value: sendSuccessful.recipient }],
|
||||
});
|
||||
expect(results.length).toBeGreaterThanOrEqual(1);
|
||||
|
||||
// Check basic structure of all results
|
||||
for (const result of results) {
|
||||
const msg = fromOneElementArray(result.tx.value.msg);
|
||||
assert(isMsgSend(msg), `${result.hash} (height ${result.height}) is not a bank send transaction`);
|
||||
expect(msg.value.to_address).toEqual(sendSuccessful.recipient);
|
||||
}
|
||||
|
||||
// Check details of most recent result
|
||||
expect(results[results.length - 1]).toEqual(
|
||||
jasmine.objectContaining({
|
||||
height: sendSuccessful.height,
|
||||
hash: sendSuccessful.hash,
|
||||
tx: sendSuccessful.tx,
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
296
packages/sdk38/src/cosmosclient.spec.ts
Normal file
296
packages/sdk38/src/cosmosclient.spec.ts
Normal file
@ -0,0 +1,296 @@
|
||||
/* eslint-disable @typescript-eslint/camelcase */
|
||||
import { sleep } from "@iov/utils";
|
||||
import { ReadonlyDate } from "readonly-date";
|
||||
|
||||
import { CosmosClient, PrivateCosmWasmClient } from "./cosmosclient";
|
||||
import { makeSignBytes } from "./encoding";
|
||||
import { findAttribute } from "./logs";
|
||||
import { Secp256k1Pen } from "./pen";
|
||||
import cosmoshub from "./testdata/cosmoshub.json";
|
||||
import {
|
||||
faucet,
|
||||
makeRandomAddress,
|
||||
pendingWithoutWasmd,
|
||||
tendermintIdMatcher,
|
||||
unused,
|
||||
wasmd,
|
||||
} from "./testutils.spec";
|
||||
import { MsgSend, StdFee } from "./types";
|
||||
|
||||
const guest = {
|
||||
address: "cosmos17d0jcz59jf68g52vq38tuuncmwwjk42u6mcxej",
|
||||
};
|
||||
|
||||
describe("CosmosClient", () => {
|
||||
describe("constructor", () => {
|
||||
it("can be constructed", () => {
|
||||
const client = new CosmosClient(wasmd.endpoint);
|
||||
expect(client).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
describe("getChainId", () => {
|
||||
it("works", async () => {
|
||||
pendingWithoutWasmd();
|
||||
const client = new CosmosClient(wasmd.endpoint);
|
||||
expect(await client.getChainId()).toEqual(wasmd.chainId);
|
||||
});
|
||||
|
||||
it("caches chain ID", async () => {
|
||||
pendingWithoutWasmd();
|
||||
const client = new CosmosClient(wasmd.endpoint);
|
||||
const openedClient = (client as unknown) as PrivateCosmWasmClient;
|
||||
const getCodeSpy = spyOn(openedClient.restClient, "nodeInfo").and.callThrough();
|
||||
|
||||
expect(await client.getChainId()).toEqual(wasmd.chainId); // from network
|
||||
expect(await client.getChainId()).toEqual(wasmd.chainId); // from cache
|
||||
|
||||
expect(getCodeSpy).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe("getHeight", () => {
|
||||
it("gets height via last block", async () => {
|
||||
pendingWithoutWasmd();
|
||||
const client = new CosmosClient(wasmd.endpoint);
|
||||
const openedClient = (client as unknown) as PrivateCosmWasmClient;
|
||||
const blockLatestSpy = spyOn(openedClient.restClient, "blocksLatest").and.callThrough();
|
||||
|
||||
const height1 = await client.getHeight();
|
||||
expect(height1).toBeGreaterThan(0);
|
||||
await sleep(1_000);
|
||||
const height2 = await client.getHeight();
|
||||
expect(height2).toEqual(height1 + 1);
|
||||
|
||||
expect(blockLatestSpy).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
it("gets height via authAccount once an address is known", async () => {
|
||||
pendingWithoutWasmd();
|
||||
const client = new CosmosClient(wasmd.endpoint);
|
||||
|
||||
const openedClient = (client as unknown) as PrivateCosmWasmClient;
|
||||
const blockLatestSpy = spyOn(openedClient.restClient, "blocksLatest").and.callThrough();
|
||||
const authAccountsSpy = spyOn(openedClient.restClient, "authAccounts").and.callThrough();
|
||||
|
||||
const height1 = await client.getHeight();
|
||||
expect(height1).toBeGreaterThan(0);
|
||||
|
||||
await client.getCodes(); // warm up the client
|
||||
|
||||
const height2 = await client.getHeight();
|
||||
expect(height2).toBeGreaterThan(0);
|
||||
await sleep(1_000);
|
||||
const height3 = await client.getHeight();
|
||||
expect(height3).toEqual(height2 + 1);
|
||||
|
||||
expect(blockLatestSpy).toHaveBeenCalledTimes(1);
|
||||
expect(authAccountsSpy).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
});
|
||||
|
||||
describe("getNonce", () => {
|
||||
it("works", async () => {
|
||||
pendingWithoutWasmd();
|
||||
const client = new CosmosClient(wasmd.endpoint);
|
||||
expect(await client.getNonce(unused.address)).toEqual({
|
||||
accountNumber: unused.accountNumber,
|
||||
sequence: unused.sequence,
|
||||
});
|
||||
});
|
||||
|
||||
it("throws for missing accounts", async () => {
|
||||
pendingWithoutWasmd();
|
||||
const client = new CosmosClient(wasmd.endpoint);
|
||||
const missing = makeRandomAddress();
|
||||
await client.getNonce(missing).then(
|
||||
() => fail("this must not succeed"),
|
||||
(error) => expect(error).toMatch(/account does not exist on chain/i),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("getAccount", () => {
|
||||
it("works", async () => {
|
||||
pendingWithoutWasmd();
|
||||
const client = new CosmosClient(wasmd.endpoint);
|
||||
expect(await client.getAccount(unused.address)).toEqual({
|
||||
address: unused.address,
|
||||
accountNumber: unused.accountNumber,
|
||||
sequence: unused.sequence,
|
||||
pubkey: undefined,
|
||||
balance: [
|
||||
{ denom: "ucosm", amount: "1000000000" },
|
||||
{ denom: "ustake", amount: "1000000000" },
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it("returns undefined for missing accounts", async () => {
|
||||
pendingWithoutWasmd();
|
||||
const client = new CosmosClient(wasmd.endpoint);
|
||||
const missing = makeRandomAddress();
|
||||
expect(await client.getAccount(missing)).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe("getBlock", () => {
|
||||
it("works for latest block", async () => {
|
||||
pendingWithoutWasmd();
|
||||
const client = new CosmosClient(wasmd.endpoint);
|
||||
const response = await client.getBlock();
|
||||
|
||||
// id
|
||||
expect(response.id).toMatch(tendermintIdMatcher);
|
||||
|
||||
// header
|
||||
expect(response.header.height).toBeGreaterThanOrEqual(1);
|
||||
expect(response.header.chainId).toEqual(await client.getChainId());
|
||||
expect(new ReadonlyDate(response.header.time).getTime()).toBeLessThan(ReadonlyDate.now());
|
||||
expect(new ReadonlyDate(response.header.time).getTime()).toBeGreaterThanOrEqual(
|
||||
ReadonlyDate.now() - 5_000,
|
||||
);
|
||||
|
||||
// txs
|
||||
expect(Array.isArray(response.txs)).toEqual(true);
|
||||
});
|
||||
|
||||
it("works for block by height", async () => {
|
||||
pendingWithoutWasmd();
|
||||
const client = new CosmosClient(wasmd.endpoint);
|
||||
const height = (await client.getBlock()).header.height;
|
||||
const response = await client.getBlock(height - 1);
|
||||
|
||||
// id
|
||||
expect(response.id).toMatch(tendermintIdMatcher);
|
||||
|
||||
// header
|
||||
expect(response.header.height).toEqual(height - 1);
|
||||
expect(response.header.chainId).toEqual(await client.getChainId());
|
||||
expect(new ReadonlyDate(response.header.time).getTime()).toBeLessThan(ReadonlyDate.now());
|
||||
expect(new ReadonlyDate(response.header.time).getTime()).toBeGreaterThanOrEqual(
|
||||
ReadonlyDate.now() - 5_000,
|
||||
);
|
||||
|
||||
// txs
|
||||
expect(Array.isArray(response.txs)).toEqual(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("getIdentifier", () => {
|
||||
it("works", async () => {
|
||||
pendingWithoutWasmd();
|
||||
const client = new CosmosClient(wasmd.endpoint);
|
||||
expect(await client.getIdentifier(cosmoshub.tx)).toEqual(cosmoshub.id);
|
||||
});
|
||||
});
|
||||
|
||||
describe("postTx", () => {
|
||||
it("works", async () => {
|
||||
pendingWithoutWasmd();
|
||||
const pen = await Secp256k1Pen.fromMnemonic(faucet.mnemonic);
|
||||
const client = new CosmosClient(wasmd.endpoint);
|
||||
|
||||
const memo = "My first contract on chain";
|
||||
const sendMsg: MsgSend = {
|
||||
type: "cosmos-sdk/MsgSend",
|
||||
value: {
|
||||
from_address: faucet.address,
|
||||
to_address: makeRandomAddress(),
|
||||
amount: [
|
||||
{
|
||||
denom: "ucosm",
|
||||
amount: "1234567",
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
const fee: StdFee = {
|
||||
amount: [
|
||||
{
|
||||
amount: "5000",
|
||||
denom: "ucosm",
|
||||
},
|
||||
],
|
||||
gas: "890000",
|
||||
};
|
||||
|
||||
const chainId = await client.getChainId();
|
||||
const { accountNumber, sequence } = await client.getNonce(faucet.address);
|
||||
const signBytes = makeSignBytes([sendMsg], fee, chainId, memo, accountNumber, sequence);
|
||||
const signature = await pen.sign(signBytes);
|
||||
const signedTx = {
|
||||
msg: [sendMsg],
|
||||
fee: fee,
|
||||
memo: memo,
|
||||
signatures: [signature],
|
||||
};
|
||||
const { logs, transactionHash } = await client.postTx(signedTx);
|
||||
const amountAttr = findAttribute(logs, "transfer", "amount");
|
||||
expect(amountAttr.value).toEqual("1234567ucosm");
|
||||
expect(transactionHash).toMatch(/^[0-9A-F]{64}$/);
|
||||
});
|
||||
});
|
||||
|
||||
describe("getContracts", () => {
|
||||
it("works", async () => {
|
||||
pendingWithoutWasmd();
|
||||
const client = new CosmosClient(wasmd.endpoint);
|
||||
const result = await client.getContracts(1);
|
||||
expect(result.length).toBeGreaterThanOrEqual(3);
|
||||
const [hash, isa, jade] = result;
|
||||
expect(hash).toEqual({
|
||||
address: "cosmos18vd8fpwxzck93qlwghaj6arh4p7c5n89uzcee5",
|
||||
codeId: 1,
|
||||
creator: faucet.address,
|
||||
label: "HASH",
|
||||
});
|
||||
expect(isa).toEqual({
|
||||
address: "cosmos1hqrdl6wstt8qzshwc6mrumpjk9338k0lr4dqxd",
|
||||
codeId: 1,
|
||||
creator: faucet.address,
|
||||
label: "ISA",
|
||||
});
|
||||
expect(jade).toEqual({
|
||||
address: "cosmos18r5szma8hm93pvx6lwpjwyxruw27e0k5uw835c",
|
||||
codeId: 1,
|
||||
creator: faucet.address,
|
||||
label: "JADE",
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("getContract", () => {
|
||||
it("works for HASH instance", async () => {
|
||||
pendingWithoutWasmd();
|
||||
const client = new CosmosClient(wasmd.endpoint);
|
||||
const hash = await client.getContract("cosmos18vd8fpwxzck93qlwghaj6arh4p7c5n89uzcee5");
|
||||
expect(hash).toEqual({
|
||||
address: "cosmos18vd8fpwxzck93qlwghaj6arh4p7c5n89uzcee5",
|
||||
codeId: 1,
|
||||
creator: faucet.address,
|
||||
label: "HASH",
|
||||
initMsg: {
|
||||
decimals: 5,
|
||||
name: "Hash token",
|
||||
symbol: "HASH",
|
||||
initial_balances: jasmine.arrayContaining([
|
||||
{
|
||||
address: faucet.address,
|
||||
amount: "11",
|
||||
},
|
||||
{
|
||||
address: unused.address,
|
||||
amount: "12812345",
|
||||
},
|
||||
{
|
||||
address: guest.address,
|
||||
amount: "22004000000",
|
||||
},
|
||||
]),
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
439
packages/sdk38/src/cosmosclient.ts
Normal file
439
packages/sdk38/src/cosmosclient.ts
Normal file
@ -0,0 +1,439 @@
|
||||
import { Sha256 } from "@iov/crypto";
|
||||
import { Encoding } from "@iov/encoding";
|
||||
|
||||
import { Coin } from "./coins";
|
||||
import { Log, parseLogs } from "./logs";
|
||||
import { decodeBech32Pubkey } from "./pubkey";
|
||||
import { BroadcastMode, RestClient } from "./restclient";
|
||||
import { CosmosSdkTx, JsonObject, PubKey, StdTx } from "./types";
|
||||
|
||||
export interface GetNonceResult {
|
||||
readonly accountNumber: number;
|
||||
readonly sequence: number;
|
||||
}
|
||||
|
||||
export interface Account {
|
||||
/** Bech32 account address */
|
||||
readonly address: string;
|
||||
readonly balance: ReadonlyArray<Coin>;
|
||||
readonly pubkey: PubKey | undefined;
|
||||
readonly accountNumber: number;
|
||||
readonly sequence: number;
|
||||
}
|
||||
|
||||
export interface PostTxResult {
|
||||
readonly logs: readonly Log[];
|
||||
readonly rawLog: string;
|
||||
/** Transaction hash (might be used as transaction ID). Guaranteed to be non-empty upper-case hex */
|
||||
readonly transactionHash: string;
|
||||
}
|
||||
|
||||
export interface SearchByIdQuery {
|
||||
readonly id: string;
|
||||
}
|
||||
|
||||
export interface SearchByHeightQuery {
|
||||
readonly height: number;
|
||||
}
|
||||
|
||||
export interface SearchBySentFromOrToQuery {
|
||||
readonly sentFromOrTo: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* This query type allows you to pass arbitrary key/value pairs to the backend. It is
|
||||
* more powerful and slightly lower level than the other search options.
|
||||
*/
|
||||
export interface SearchByTagsQuery {
|
||||
readonly tags: readonly { readonly key: string; readonly value: string }[];
|
||||
}
|
||||
|
||||
export type SearchTxQuery =
|
||||
| SearchByIdQuery
|
||||
| SearchByHeightQuery
|
||||
| SearchBySentFromOrToQuery
|
||||
| SearchByTagsQuery;
|
||||
|
||||
function isSearchByIdQuery(query: SearchTxQuery): query is SearchByIdQuery {
|
||||
return (query as SearchByIdQuery).id !== undefined;
|
||||
}
|
||||
|
||||
function isSearchByHeightQuery(query: SearchTxQuery): query is SearchByHeightQuery {
|
||||
return (query as SearchByHeightQuery).height !== undefined;
|
||||
}
|
||||
|
||||
function isSearchBySentFromOrToQuery(query: SearchTxQuery): query is SearchBySentFromOrToQuery {
|
||||
return (query as SearchBySentFromOrToQuery).sentFromOrTo !== undefined;
|
||||
}
|
||||
|
||||
function isSearchByTagsQuery(query: SearchTxQuery): query is SearchByTagsQuery {
|
||||
return (query as SearchByTagsQuery).tags !== undefined;
|
||||
}
|
||||
|
||||
export interface SearchTxFilter {
|
||||
readonly minHeight?: number;
|
||||
readonly maxHeight?: number;
|
||||
}
|
||||
|
||||
export interface Code {
|
||||
readonly id: number;
|
||||
/** Bech32 account address */
|
||||
readonly creator: string;
|
||||
/** Hex-encoded sha256 hash of the code stored here */
|
||||
readonly checksum: string;
|
||||
readonly source?: string;
|
||||
readonly builder?: string;
|
||||
}
|
||||
|
||||
export interface CodeDetails extends Code {
|
||||
/** The original wasm bytes */
|
||||
readonly data: Uint8Array;
|
||||
}
|
||||
|
||||
export interface Contract {
|
||||
readonly address: string;
|
||||
readonly codeId: number;
|
||||
/** Bech32 account address */
|
||||
readonly creator: string;
|
||||
readonly label: string;
|
||||
}
|
||||
|
||||
export interface ContractDetails extends Contract {
|
||||
/** Argument passed on initialization of the contract */
|
||||
readonly initMsg: object;
|
||||
}
|
||||
|
||||
/** A transaction that is indexed as part of the transaction history */
|
||||
export interface IndexedTx {
|
||||
readonly height: number;
|
||||
/** Transaction hash (might be used as transaction ID). Guaranteed to be non-empty upper-case hex */
|
||||
readonly hash: string;
|
||||
/** Transaction execution error code. 0 on success. */
|
||||
readonly code: number;
|
||||
readonly rawLog: string;
|
||||
readonly logs: readonly Log[];
|
||||
readonly tx: CosmosSdkTx;
|
||||
/** The gas limit as set by the user */
|
||||
readonly gasWanted?: number;
|
||||
/** The gas used by the execution */
|
||||
readonly gasUsed?: number;
|
||||
/** An RFC 3339 time string like e.g. '2020-02-15T10:39:10.4696305Z' */
|
||||
readonly timestamp: string;
|
||||
}
|
||||
|
||||
export interface BlockHeader {
|
||||
readonly version: {
|
||||
readonly block: string;
|
||||
readonly app: string;
|
||||
};
|
||||
readonly height: number;
|
||||
readonly chainId: string;
|
||||
/** An RFC 3339 time string like e.g. '2020-02-15T10:39:10.4696305Z' */
|
||||
readonly time: string;
|
||||
}
|
||||
|
||||
export interface Block {
|
||||
/** The ID is a hash of the block header (uppercase hex) */
|
||||
readonly id: string;
|
||||
readonly header: BlockHeader;
|
||||
/** Array of raw transactions */
|
||||
readonly txs: ReadonlyArray<Uint8Array>;
|
||||
}
|
||||
|
||||
/** Use for testing only */
|
||||
export interface PrivateCosmWasmClient {
|
||||
readonly restClient: RestClient;
|
||||
}
|
||||
|
||||
export class CosmosClient {
|
||||
protected readonly restClient: RestClient;
|
||||
/** Any address the chain considers valid (valid bech32 with proper prefix) */
|
||||
protected anyValidAddress: string | undefined;
|
||||
|
||||
private readonly codesCache = new Map<number, CodeDetails>();
|
||||
private chainId: string | undefined;
|
||||
|
||||
/**
|
||||
* Creates a new client to interact with a CosmWasm blockchain.
|
||||
*
|
||||
* This instance does a lot of caching. In order to benefit from that you should try to use one instance
|
||||
* for the lifetime of your application. When switching backends, a new instance must be created.
|
||||
*
|
||||
* @param apiUrl The URL of a Cosmos SDK light client daemon API (sometimes called REST server or REST API)
|
||||
* @param broadcastMode Defines at which point of the transaction processing the postTx method (i.e. transaction broadcasting) returns
|
||||
*/
|
||||
public constructor(apiUrl: string, broadcastMode = BroadcastMode.Block) {
|
||||
this.restClient = new RestClient(apiUrl, broadcastMode);
|
||||
}
|
||||
|
||||
public async getChainId(): Promise<string> {
|
||||
if (!this.chainId) {
|
||||
const response = await this.restClient.nodeInfo();
|
||||
const chainId = response.node_info.network;
|
||||
if (!chainId) throw new Error("Chain ID must not be empty");
|
||||
this.chainId = chainId;
|
||||
}
|
||||
|
||||
return this.chainId;
|
||||
}
|
||||
|
||||
public async getHeight(): Promise<number> {
|
||||
if (this.anyValidAddress) {
|
||||
const { height } = await this.restClient.authAccounts(this.anyValidAddress);
|
||||
return parseInt(height, 10);
|
||||
} else {
|
||||
// Note: this gets inefficient when blocks contain a lot of transactions since it
|
||||
// requires downloading and deserializing all transactions in the block.
|
||||
const latest = await this.restClient.blocksLatest();
|
||||
return parseInt(latest.block.header.height, 10);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a 32 byte upper-case hex transaction hash (typically used as the transaction ID)
|
||||
*/
|
||||
public async getIdentifier(tx: CosmosSdkTx): Promise<string> {
|
||||
// We consult the REST API because we don't have a local amino encoder
|
||||
const bytes = await this.restClient.encodeTx(tx);
|
||||
const hash = new Sha256(bytes).digest();
|
||||
return Encoding.toHex(hash).toUpperCase();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns account number and sequence.
|
||||
*
|
||||
* Throws if the account does not exist on chain.
|
||||
*
|
||||
* @param address returns data for this address. When unset, the client's sender adddress is used.
|
||||
*/
|
||||
public async getNonce(address: string): Promise<GetNonceResult> {
|
||||
const account = await this.getAccount(address);
|
||||
if (!account) {
|
||||
throw new Error(
|
||||
"Account does not exist on chain. Send some tokens there before trying to query nonces.",
|
||||
);
|
||||
}
|
||||
return {
|
||||
accountNumber: account.accountNumber,
|
||||
sequence: account.sequence,
|
||||
};
|
||||
}
|
||||
|
||||
public async getAccount(address: string): Promise<Account | undefined> {
|
||||
const account = await this.restClient.authAccounts(address);
|
||||
const value = account.result.value;
|
||||
if (value.address === "") {
|
||||
return undefined;
|
||||
} else {
|
||||
this.anyValidAddress = value.address;
|
||||
return {
|
||||
address: value.address,
|
||||
balance: value.coins,
|
||||
pubkey: value.public_key ? decodeBech32Pubkey(value.public_key) : undefined,
|
||||
accountNumber: value.account_number,
|
||||
sequence: value.sequence,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets block header and meta
|
||||
*
|
||||
* @param height The height of the block. If undefined, the latest height is used.
|
||||
*/
|
||||
public async getBlock(height?: number): Promise<Block> {
|
||||
const response =
|
||||
height !== undefined ? await this.restClient.blocks(height) : await this.restClient.blocksLatest();
|
||||
|
||||
return {
|
||||
id: response.block_id.hash,
|
||||
header: {
|
||||
version: response.block.header.version,
|
||||
time: response.block.header.time,
|
||||
height: parseInt(response.block.header.height, 10),
|
||||
chainId: response.block.header.chain_id,
|
||||
},
|
||||
txs: (response.block.data.txs || []).map((encoded) => Encoding.fromBase64(encoded)),
|
||||
};
|
||||
}
|
||||
|
||||
public async searchTx(query: SearchTxQuery, filter: SearchTxFilter = {}): Promise<readonly IndexedTx[]> {
|
||||
const minHeight = filter.minHeight || 0;
|
||||
const maxHeight = filter.maxHeight || Number.MAX_SAFE_INTEGER;
|
||||
|
||||
if (maxHeight < minHeight) return []; // optional optimization
|
||||
|
||||
function withFilters(originalQuery: string): string {
|
||||
return `${originalQuery}&tx.minheight=${minHeight}&tx.maxheight=${maxHeight}`;
|
||||
}
|
||||
|
||||
let txs: readonly IndexedTx[];
|
||||
if (isSearchByIdQuery(query)) {
|
||||
txs = await this.txsQuery(`tx.hash=${query.id}`);
|
||||
} else if (isSearchByHeightQuery(query)) {
|
||||
// optional optimization to avoid network request
|
||||
if (query.height < minHeight || query.height > maxHeight) {
|
||||
txs = [];
|
||||
} else {
|
||||
txs = await this.txsQuery(`tx.height=${query.height}`);
|
||||
}
|
||||
} else if (isSearchBySentFromOrToQuery(query)) {
|
||||
// We cannot get both in one request (see https://github.com/cosmos/gaia/issues/75)
|
||||
const sentQuery = withFilters(`message.module=bank&message.sender=${query.sentFromOrTo}`);
|
||||
const receivedQuery = withFilters(`message.module=bank&transfer.recipient=${query.sentFromOrTo}`);
|
||||
const sent = await this.txsQuery(sentQuery);
|
||||
const received = await this.txsQuery(receivedQuery);
|
||||
|
||||
const sentHashes = sent.map((t) => t.hash);
|
||||
txs = [...sent, ...received.filter((t) => !sentHashes.includes(t.hash))];
|
||||
} else if (isSearchByTagsQuery(query)) {
|
||||
const rawQuery = withFilters(query.tags.map((t) => `${t.key}=${t.value}`).join("&"));
|
||||
txs = await this.txsQuery(rawQuery);
|
||||
} else {
|
||||
throw new Error("Unknown query type");
|
||||
}
|
||||
|
||||
// backend sometimes messes up with min/max height filtering
|
||||
const filtered = txs.filter((tx) => tx.height >= minHeight && tx.height <= maxHeight);
|
||||
|
||||
return filtered;
|
||||
}
|
||||
|
||||
public async postTx(tx: StdTx): Promise<PostTxResult> {
|
||||
const result = await this.restClient.postTx(tx);
|
||||
if (!result.txhash.match(/^([0-9A-F][0-9A-F])+$/)) {
|
||||
throw new Error("Received ill-formatted txhash. Must be non-empty upper-case hex");
|
||||
}
|
||||
|
||||
if (result.code) {
|
||||
throw new Error(
|
||||
`Error when posting tx ${result.txhash}. Code: ${result.code}; Raw log: ${result.raw_log}`,
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
logs: result.logs ? parseLogs(result.logs) : [],
|
||||
rawLog: result.raw_log || "",
|
||||
transactionHash: result.txhash,
|
||||
};
|
||||
}
|
||||
|
||||
public async getCodes(): Promise<readonly Code[]> {
|
||||
const result = await this.restClient.listCodeInfo();
|
||||
return result.map(
|
||||
(entry): Code => {
|
||||
this.anyValidAddress = entry.creator;
|
||||
return {
|
||||
id: entry.id,
|
||||
creator: entry.creator,
|
||||
checksum: Encoding.toHex(Encoding.fromHex(entry.data_hash)),
|
||||
source: entry.source || undefined,
|
||||
builder: entry.builder || undefined,
|
||||
};
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
public async getCodeDetails(codeId: number): Promise<CodeDetails> {
|
||||
const cached = this.codesCache.get(codeId);
|
||||
if (cached) return cached;
|
||||
|
||||
const getCodeResult = await this.restClient.getCode(codeId);
|
||||
const codeDetails: CodeDetails = {
|
||||
id: getCodeResult.id,
|
||||
creator: getCodeResult.creator,
|
||||
checksum: Encoding.toHex(Encoding.fromHex(getCodeResult.data_hash)),
|
||||
source: getCodeResult.source || undefined,
|
||||
builder: getCodeResult.builder || undefined,
|
||||
data: Encoding.fromBase64(getCodeResult.data),
|
||||
};
|
||||
this.codesCache.set(codeId, codeDetails);
|
||||
return codeDetails;
|
||||
}
|
||||
|
||||
public async getContracts(codeId: number): Promise<readonly Contract[]> {
|
||||
const result = await this.restClient.listContractsByCodeId(codeId);
|
||||
return result.map(
|
||||
(entry): Contract => ({
|
||||
address: entry.address,
|
||||
codeId: entry.code_id,
|
||||
creator: entry.creator,
|
||||
label: entry.label,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Throws an error if no contract was found at the address
|
||||
*/
|
||||
public async getContract(address: string): Promise<ContractDetails> {
|
||||
const result = await this.restClient.getContractInfo(address);
|
||||
if (!result) throw new Error(`No contract found at address "${address}"`);
|
||||
return {
|
||||
address: result.address,
|
||||
codeId: result.code_id,
|
||||
creator: result.creator,
|
||||
label: result.label,
|
||||
initMsg: result.init_msg,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the data at the key if present (raw contract dependent storage data)
|
||||
* or null if no data at this key.
|
||||
*
|
||||
* Promise is rejected when contract does not exist.
|
||||
*/
|
||||
public async queryContractRaw(address: string, key: Uint8Array): Promise<Uint8Array | null> {
|
||||
// just test contract existence
|
||||
const _info = await this.getContract(address);
|
||||
|
||||
return this.restClient.queryContractRaw(address, key);
|
||||
}
|
||||
|
||||
/**
|
||||
* Makes a smart query on the contract, returns the parsed JSON document.
|
||||
*
|
||||
* Promise is rejected when contract does not exist.
|
||||
* Promise is rejected for invalid query format.
|
||||
* Promise is rejected for invalid response format.
|
||||
*/
|
||||
public async queryContractSmart(address: string, queryMsg: object): Promise<JsonObject> {
|
||||
try {
|
||||
return await this.restClient.queryContractSmart(address, queryMsg);
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
if (error.message.startsWith("not found: contract")) {
|
||||
throw new Error(`No contract found at address "${address}"`);
|
||||
} else {
|
||||
throw error;
|
||||
}
|
||||
} else {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async txsQuery(query: string): Promise<readonly IndexedTx[]> {
|
||||
// TODO: we need proper pagination support
|
||||
const limit = 100;
|
||||
const result = await this.restClient.txsQuery(`${query}&limit=${limit}`);
|
||||
const pages = parseInt(result.page_total, 10);
|
||||
if (pages > 1) {
|
||||
throw new Error(
|
||||
`Found more results on the backend than we can process currently. Results: ${result.total_count}, supported: ${limit}`,
|
||||
);
|
||||
}
|
||||
return result.txs.map(
|
||||
(restItem): IndexedTx => ({
|
||||
height: parseInt(restItem.height, 10),
|
||||
hash: restItem.txhash,
|
||||
code: restItem.code || 0,
|
||||
rawLog: restItem.raw_log,
|
||||
logs: parseLogs(restItem.logs || []),
|
||||
tx: restItem.tx,
|
||||
timestamp: restItem.timestamp,
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
11
packages/sdk38/src/decoding.ts
Normal file
11
packages/sdk38/src/decoding.ts
Normal file
@ -0,0 +1,11 @@
|
||||
import { Encoding } from "@iov/encoding";
|
||||
|
||||
import { isStdTx, StdTx } from "./types";
|
||||
|
||||
export function unmarshalTx(data: Uint8Array): StdTx {
|
||||
const decoded = JSON.parse(Encoding.fromUtf8(data));
|
||||
if (!isStdTx(decoded)) {
|
||||
throw new Error("Must be json encoded StdTx");
|
||||
}
|
||||
return decoded;
|
||||
}
|
||||
1
packages/sdk38/src/encoding.spec.ts
Normal file
1
packages/sdk38/src/encoding.spec.ts
Normal file
@ -0,0 +1 @@
|
||||
describe("encoding", () => {});
|
||||
59
packages/sdk38/src/encoding.ts
Normal file
59
packages/sdk38/src/encoding.ts
Normal file
@ -0,0 +1,59 @@
|
||||
import { Encoding } from "@iov/encoding";
|
||||
|
||||
import { Msg, StdFee, StdTx } from "./types";
|
||||
|
||||
const { toUtf8 } = Encoding;
|
||||
|
||||
function sortJson(json: any): any {
|
||||
if (typeof json !== "object" || json === null) {
|
||||
return json;
|
||||
}
|
||||
if (Array.isArray(json)) {
|
||||
return json.map(sortJson);
|
||||
}
|
||||
const sortedKeys = Object.keys(json).sort();
|
||||
const result = sortedKeys.reduce(
|
||||
(accumulator, key) => ({
|
||||
...accumulator,
|
||||
[key]: sortJson(json[key]),
|
||||
}),
|
||||
{},
|
||||
);
|
||||
return result;
|
||||
}
|
||||
|
||||
export function marshalTx(tx: StdTx): Uint8Array {
|
||||
const json = JSON.stringify(tx);
|
||||
return Encoding.toUtf8(json);
|
||||
}
|
||||
|
||||
interface SignJson {
|
||||
readonly account_number: string;
|
||||
readonly chain_id: string;
|
||||
readonly fee: StdFee;
|
||||
readonly memo: string;
|
||||
readonly msgs: readonly Msg[];
|
||||
readonly sequence: string;
|
||||
}
|
||||
|
||||
export function makeSignBytes(
|
||||
msgs: readonly Msg[],
|
||||
fee: StdFee,
|
||||
chainId: string,
|
||||
memo: string,
|
||||
accountNumber: number,
|
||||
sequence: number,
|
||||
): Uint8Array {
|
||||
const signJson: SignJson = {
|
||||
// eslint-disable-next-line @typescript-eslint/camelcase
|
||||
account_number: accountNumber.toString(),
|
||||
// eslint-disable-next-line @typescript-eslint/camelcase
|
||||
chain_id: chainId,
|
||||
fee: fee,
|
||||
memo: memo,
|
||||
msgs: msgs,
|
||||
sequence: sequence.toString(),
|
||||
};
|
||||
const signMsg = sortJson(signJson);
|
||||
return toUtf8(JSON.stringify(signMsg));
|
||||
}
|
||||
12
packages/sdk38/src/index.ts
Normal file
12
packages/sdk38/src/index.ts
Normal file
@ -0,0 +1,12 @@
|
||||
import * as logs from "./logs";
|
||||
import * as types from "./types";
|
||||
export { logs, types };
|
||||
|
||||
export { pubkeyToAddress } from "./address";
|
||||
export { Coin, coin, coins } from "./coins";
|
||||
export { unmarshalTx } from "./decoding";
|
||||
export { makeSignBytes, marshalTx } from "./encoding";
|
||||
export { BroadcastMode, RestClient, TxsResponse } from "./restclient";
|
||||
export { decodeBech32Pubkey, encodeBech32Pubkey, encodeSecp256k1Pubkey } from "./pubkey";
|
||||
export { findSequenceForSignedTx } from "./sequence";
|
||||
export { encodeSecp256k1Signature, decodeSignature } from "./signature";
|
||||
165
packages/sdk38/src/logs.spec.ts
Normal file
165
packages/sdk38/src/logs.spec.ts
Normal file
@ -0,0 +1,165 @@
|
||||
/* eslint-disable @typescript-eslint/camelcase */
|
||||
import { parseAttribute, parseEvent, parseLog, parseLogs } from "./logs";
|
||||
|
||||
describe("logs", () => {
|
||||
describe("parseAttribute", () => {
|
||||
it("works", () => {
|
||||
const attr = parseAttribute({ key: "a", value: "b" });
|
||||
expect(attr).toEqual({ key: "a", value: "b" });
|
||||
});
|
||||
|
||||
it("works for empty value", () => {
|
||||
const attr = parseAttribute({ key: "foobar", value: "" });
|
||||
expect(attr).toEqual({ key: "foobar", value: "" });
|
||||
});
|
||||
|
||||
it("normalized unset value to empty string", () => {
|
||||
const attr = parseAttribute({ key: "amount" });
|
||||
expect(attr).toEqual({ key: "amount", value: "" });
|
||||
});
|
||||
});
|
||||
|
||||
describe("parseEvent", () => {
|
||||
it("works", () => {
|
||||
const original = {
|
||||
type: "message",
|
||||
attributes: [
|
||||
{
|
||||
key: "action",
|
||||
value: "store-code",
|
||||
},
|
||||
{
|
||||
key: "module",
|
||||
value: "wasm",
|
||||
},
|
||||
{
|
||||
key: "action",
|
||||
value: "store-code",
|
||||
},
|
||||
{
|
||||
key: "sender",
|
||||
value: "cosmos1pkptre7fdkl6gfrzlesjjvhxhlc3r4gmmk8rs6",
|
||||
},
|
||||
{
|
||||
key: "code_id",
|
||||
value: "1",
|
||||
},
|
||||
],
|
||||
} as const;
|
||||
|
||||
const event = parseEvent(original);
|
||||
expect(event).toEqual(original);
|
||||
});
|
||||
|
||||
it("works for transfer event", () => {
|
||||
const original = {
|
||||
type: "transfer",
|
||||
attributes: [
|
||||
{
|
||||
key: "recipient",
|
||||
value: "cosmos18vd8fpwxzck93qlwghaj6arh4p7c5n89uzcee5",
|
||||
},
|
||||
{
|
||||
key: "amount",
|
||||
},
|
||||
],
|
||||
} as const;
|
||||
const expected = {
|
||||
type: "transfer",
|
||||
attributes: [
|
||||
{
|
||||
key: "recipient",
|
||||
value: "cosmos18vd8fpwxzck93qlwghaj6arh4p7c5n89uzcee5",
|
||||
},
|
||||
{
|
||||
key: "amount",
|
||||
value: "",
|
||||
},
|
||||
],
|
||||
} as const;
|
||||
|
||||
const event = parseEvent(original);
|
||||
expect(event).toEqual(expected);
|
||||
});
|
||||
});
|
||||
|
||||
describe("parseLog", () => {
|
||||
it("works", () => {
|
||||
const original = {
|
||||
msg_index: 0,
|
||||
log: "",
|
||||
events: [
|
||||
{
|
||||
type: "message",
|
||||
attributes: [
|
||||
{
|
||||
key: "action",
|
||||
value: "store-code",
|
||||
},
|
||||
{
|
||||
key: "module",
|
||||
value: "wasm",
|
||||
},
|
||||
{
|
||||
key: "action",
|
||||
value: "store-code",
|
||||
},
|
||||
{
|
||||
key: "sender",
|
||||
value: "cosmos1pkptre7fdkl6gfrzlesjjvhxhlc3r4gmmk8rs6",
|
||||
},
|
||||
{
|
||||
key: "code_id",
|
||||
value: "1",
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
} as const;
|
||||
|
||||
const log = parseLog(original);
|
||||
expect(log).toEqual(original);
|
||||
});
|
||||
});
|
||||
|
||||
describe("parseLogs", () => {
|
||||
it("works", () => {
|
||||
const original = [
|
||||
{
|
||||
msg_index: 0,
|
||||
log: "",
|
||||
events: [
|
||||
{
|
||||
type: "message",
|
||||
attributes: [
|
||||
{
|
||||
key: "action",
|
||||
value: "store-code",
|
||||
},
|
||||
{
|
||||
key: "module",
|
||||
value: "wasm",
|
||||
},
|
||||
{
|
||||
key: "action",
|
||||
value: "store-code",
|
||||
},
|
||||
{
|
||||
key: "sender",
|
||||
value: "cosmos1pkptre7fdkl6gfrzlesjjvhxhlc3r4gmmk8rs6",
|
||||
},
|
||||
{
|
||||
key: "code_id",
|
||||
value: "1",
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
] as const;
|
||||
|
||||
const logs = parseLogs(original);
|
||||
expect(logs).toEqual(original);
|
||||
});
|
||||
});
|
||||
});
|
||||
86
packages/sdk38/src/logs.ts
Normal file
86
packages/sdk38/src/logs.ts
Normal file
@ -0,0 +1,86 @@
|
||||
/* eslint-disable @typescript-eslint/camelcase */
|
||||
import { isNonNullObject } from "@iov/encoding";
|
||||
|
||||
export interface Attribute {
|
||||
readonly key: string;
|
||||
readonly value: string;
|
||||
}
|
||||
|
||||
export interface Event {
|
||||
readonly type: string;
|
||||
readonly attributes: readonly Attribute[];
|
||||
}
|
||||
|
||||
export interface Log {
|
||||
readonly msg_index: number;
|
||||
readonly log: string;
|
||||
readonly events: readonly Event[];
|
||||
}
|
||||
|
||||
export function parseAttribute(input: unknown): Attribute {
|
||||
if (!isNonNullObject(input)) throw new Error("Attribute must be a non-null object");
|
||||
const { key, value } = input as any;
|
||||
if (typeof key !== "string" || !key) throw new Error("Attribute's key must be a non-empty string");
|
||||
if (typeof value !== "string" && typeof value !== "undefined") {
|
||||
throw new Error("Attribute's value must be a string or unset");
|
||||
}
|
||||
|
||||
return {
|
||||
key: key,
|
||||
value: value || "",
|
||||
};
|
||||
}
|
||||
|
||||
export function parseEvent(input: unknown): Event {
|
||||
if (!isNonNullObject(input)) throw new Error("Event must be a non-null object");
|
||||
const { type, attributes } = input as any;
|
||||
if (typeof type !== "string" || type === "") {
|
||||
throw new Error(`Event type must be a non-empty string`);
|
||||
}
|
||||
if (!Array.isArray(attributes)) throw new Error("Event's attributes must be an array");
|
||||
return {
|
||||
type: type,
|
||||
attributes: attributes.map(parseAttribute),
|
||||
};
|
||||
}
|
||||
|
||||
export function parseLog(input: unknown): Log {
|
||||
if (!isNonNullObject(input)) throw new Error("Log must be a non-null object");
|
||||
const { msg_index, log, events } = input as any;
|
||||
if (typeof msg_index !== "number") throw new Error("Log's msg_index must be a number");
|
||||
if (typeof log !== "string") throw new Error("Log's log must be a string");
|
||||
if (!Array.isArray(events)) throw new Error("Log's events must be an array");
|
||||
return {
|
||||
msg_index: msg_index,
|
||||
log: log,
|
||||
events: events.map(parseEvent),
|
||||
};
|
||||
}
|
||||
|
||||
export function parseLogs(input: unknown): readonly Log[] {
|
||||
if (!Array.isArray(input)) throw new Error("Logs must be an array");
|
||||
return input.map(parseLog);
|
||||
}
|
||||
|
||||
/**
|
||||
* Searches in logs for the first event of the given event type and in that event
|
||||
* for the first first attribute with the given attribute key.
|
||||
*
|
||||
* Throws if the attribute was not found.
|
||||
*/
|
||||
export function findAttribute(
|
||||
logs: readonly Log[],
|
||||
eventType: "message" | "transfer",
|
||||
attrKey: string,
|
||||
): Attribute {
|
||||
const firstLogs = logs.find(() => true);
|
||||
const out = firstLogs?.events
|
||||
.find((event) => event.type === eventType)
|
||||
?.attributes.find((attr) => attr.key === attrKey);
|
||||
if (!out) {
|
||||
throw new Error(
|
||||
`Could not find attribute '${attrKey}' in first event of type '${eventType}' in first log.`,
|
||||
);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
56
packages/sdk38/src/pen.spec.ts
Normal file
56
packages/sdk38/src/pen.spec.ts
Normal file
@ -0,0 +1,56 @@
|
||||
import { Secp256k1, Secp256k1Signature, Sha256 } from "@iov/crypto";
|
||||
import { Encoding } from "@iov/encoding";
|
||||
|
||||
import { Secp256k1Pen } from "./pen";
|
||||
import { decodeSignature } from "./signature";
|
||||
|
||||
const { fromHex } = Encoding;
|
||||
|
||||
describe("Sec256k1Pen", () => {
|
||||
it("can be constructed", async () => {
|
||||
const pen = await Secp256k1Pen.fromMnemonic(
|
||||
"zebra slush diet army arrest purpose hawk source west glimpse custom record",
|
||||
);
|
||||
expect(pen).toBeTruthy();
|
||||
});
|
||||
|
||||
describe("pubkey", () => {
|
||||
it("returns compressed pubkey", async () => {
|
||||
// special sign fit simple patrol salute grocery chicken wheat radar tonight ceiling
|
||||
// m/44'/118'/0'/0/0
|
||||
// pubkey: 02baa4ef93f2ce84592a49b1d729c074eab640112522a7a89f7d03ebab21ded7b6
|
||||
const pen = await Secp256k1Pen.fromMnemonic(
|
||||
"special sign fit simple patrol salute grocery chicken wheat radar tonight ceiling",
|
||||
);
|
||||
expect(pen.pubkey).toEqual(
|
||||
fromHex("02baa4ef93f2ce84592a49b1d729c074eab640112522a7a89f7d03ebab21ded7b6"),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("sign", () => {
|
||||
it("creates correct signatures", async () => {
|
||||
const pen = await Secp256k1Pen.fromMnemonic(
|
||||
"special sign fit simple patrol salute grocery chicken wheat radar tonight ceiling",
|
||||
);
|
||||
const data = Encoding.toAscii("foo bar");
|
||||
const { pubkey, signature } = decodeSignature(await pen.sign(data));
|
||||
|
||||
const valid = await Secp256k1.verifySignature(
|
||||
Secp256k1Signature.fromFixedLength(signature),
|
||||
new Sha256(data).digest(),
|
||||
pubkey,
|
||||
);
|
||||
expect(valid).toEqual(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("address", () => {
|
||||
it("creates same address as Go imlementation", async () => {
|
||||
const pen = await Secp256k1Pen.fromMnemonic(
|
||||
"oyster design unusual machine spread century engine gravity focus cave carry slot",
|
||||
);
|
||||
expect(pen.address("cosmos")).toEqual("cosmos1cjsxept9rkggzxztslae9ndgpdyt2408lk850u");
|
||||
});
|
||||
});
|
||||
});
|
||||
92
packages/sdk38/src/pen.ts
Normal file
92
packages/sdk38/src/pen.ts
Normal file
@ -0,0 +1,92 @@
|
||||
import {
|
||||
Bip39,
|
||||
EnglishMnemonic,
|
||||
Secp256k1,
|
||||
Sha256,
|
||||
Sha512,
|
||||
Slip10,
|
||||
Slip10Curve,
|
||||
Slip10RawIndex,
|
||||
} from "@iov/crypto";
|
||||
|
||||
import { rawSecp256k1PubkeyToAddress } from "./address";
|
||||
import { encodeSecp256k1Signature } from "./signature";
|
||||
import { StdSignature } from "./types";
|
||||
|
||||
export type PrehashType = "sha256" | "sha512" | null;
|
||||
|
||||
/**
|
||||
* A pen is the most basic tool you can think of for signing. It works
|
||||
* everywhere and can be used intuitively by everyone. However, it does not
|
||||
* come with a great amount of features. End of semi suitable metaphor.
|
||||
*
|
||||
* This wraps a single keypair and allows for signing.
|
||||
*
|
||||
* Non-goals of this types are: multi account support, persistency, data migrations,
|
||||
* obfuscation of sensitive data.
|
||||
*/
|
||||
export interface Pen {
|
||||
readonly pubkey: Uint8Array;
|
||||
readonly sign: (signBytes: Uint8Array, prehashType?: PrehashType) => Promise<StdSignature>;
|
||||
}
|
||||
|
||||
function prehash(bytes: Uint8Array, type: PrehashType): Uint8Array {
|
||||
switch (type) {
|
||||
case null:
|
||||
return new Uint8Array([...bytes]);
|
||||
case "sha256":
|
||||
return new Sha256(bytes).digest();
|
||||
case "sha512":
|
||||
return new Sha512(bytes).digest();
|
||||
default:
|
||||
throw new Error("Unknown prehash type");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The Cosmoshub derivation path in the form `m/44'/118'/0'/0/a`
|
||||
* with 0-based account index `a`.
|
||||
*/
|
||||
export function makeCosmoshubPath(a: number): readonly Slip10RawIndex[] {
|
||||
return [
|
||||
Slip10RawIndex.hardened(44),
|
||||
Slip10RawIndex.hardened(118),
|
||||
Slip10RawIndex.hardened(0),
|
||||
Slip10RawIndex.normal(0),
|
||||
Slip10RawIndex.normal(a),
|
||||
];
|
||||
}
|
||||
|
||||
export class Secp256k1Pen implements Pen {
|
||||
public static async fromMnemonic(
|
||||
mnemonic: string,
|
||||
hdPath: readonly Slip10RawIndex[] = makeCosmoshubPath(0),
|
||||
): Promise<Secp256k1Pen> {
|
||||
const seed = await Bip39.mnemonicToSeed(new EnglishMnemonic(mnemonic));
|
||||
const { privkey } = Slip10.derivePath(Slip10Curve.Secp256k1, seed, hdPath);
|
||||
const uncompressed = (await Secp256k1.makeKeypair(privkey)).pubkey;
|
||||
return new Secp256k1Pen(privkey, Secp256k1.compressPubkey(uncompressed));
|
||||
}
|
||||
|
||||
public readonly pubkey: Uint8Array;
|
||||
private readonly privkey: Uint8Array;
|
||||
|
||||
private constructor(privkey: Uint8Array, pubkey: Uint8Array) {
|
||||
this.privkey = privkey;
|
||||
this.pubkey = pubkey;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates and returns a signature
|
||||
*/
|
||||
public async sign(signBytes: Uint8Array, prehashType: PrehashType = "sha256"): Promise<StdSignature> {
|
||||
const message = prehash(signBytes, prehashType);
|
||||
const signature = await Secp256k1.createSignature(message, this.privkey);
|
||||
const fixedLengthSignature = new Uint8Array([...signature.r(32), ...signature.s(32)]);
|
||||
return encodeSecp256k1Signature(this.pubkey, fixedLengthSignature);
|
||||
}
|
||||
|
||||
public address(prefix: string): string {
|
||||
return rawSecp256k1PubkeyToAddress(this.pubkey, prefix);
|
||||
}
|
||||
}
|
||||
57
packages/sdk38/src/pubkey.spec.ts
Normal file
57
packages/sdk38/src/pubkey.spec.ts
Normal file
@ -0,0 +1,57 @@
|
||||
import { Encoding } from "@iov/encoding";
|
||||
|
||||
import { decodeBech32Pubkey, encodeBech32Pubkey, encodeSecp256k1Pubkey } from "./pubkey";
|
||||
import { PubKey } from "./types";
|
||||
|
||||
const { fromBase64 } = Encoding;
|
||||
|
||||
describe("pubkey", () => {
|
||||
describe("encodeSecp256k1Pubkey", () => {
|
||||
it("encodes a compresed pubkey", () => {
|
||||
const pubkey = fromBase64("AtQaCqFnshaZQp6rIkvAPyzThvCvXSDO+9AzbxVErqJP");
|
||||
expect(encodeSecp256k1Pubkey(pubkey)).toEqual({
|
||||
type: "tendermint/PubKeySecp256k1",
|
||||
value: "AtQaCqFnshaZQp6rIkvAPyzThvCvXSDO+9AzbxVErqJP",
|
||||
});
|
||||
});
|
||||
|
||||
it("throws for uncompressed public keys", () => {
|
||||
const pubkey = fromBase64(
|
||||
"BE8EGB7ro1ORuFhjOnZcSgwYlpe0DSFjVNUIkNNQxwKQE7WHpoHoNswYeoFkuYpYSKK4mzFzMV/dB0DVAy4lnNU=",
|
||||
);
|
||||
expect(() => encodeSecp256k1Pubkey(pubkey)).toThrowError(/public key must be compressed secp256k1/i);
|
||||
});
|
||||
});
|
||||
|
||||
describe("decodeBech32Pubkey", () => {
|
||||
it("works", () => {
|
||||
expect(
|
||||
decodeBech32Pubkey("cosmospub1addwnpepqd8sgxq7aw348ydctp3n5ajufgxp395hksxjzc6565yfp56scupfqhlgyg5"),
|
||||
).toEqual({
|
||||
type: "tendermint/PubKeySecp256k1",
|
||||
value: "A08EGB7ro1ORuFhjOnZcSgwYlpe0DSFjVNUIkNNQxwKQ",
|
||||
});
|
||||
});
|
||||
|
||||
it("works for enigma pubkey", () => {
|
||||
expect(
|
||||
decodeBech32Pubkey("enigmapub1addwnpepqw5k9p439nw0zpg2aundx4umwx4nw233z5prpjqjv5anl5grmnchzp2xwvv"),
|
||||
).toEqual({
|
||||
type: "tendermint/PubKeySecp256k1",
|
||||
value: "A6lihrEs3PEFCu8m01ebcas3KjEVAjDIEmU7P9ED3PFx",
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("encodeBech32Pubkey", () => {
|
||||
it("works for secp256k1", () => {
|
||||
const pubkey: PubKey = {
|
||||
type: "tendermint/PubKeySecp256k1",
|
||||
value: "A08EGB7ro1ORuFhjOnZcSgwYlpe0DSFjVNUIkNNQxwKQ",
|
||||
};
|
||||
expect(encodeBech32Pubkey(pubkey, "cosmospub")).toEqual(
|
||||
"cosmospub1addwnpepqd8sgxq7aw348ydctp3n5ajufgxp395hksxjzc6565yfp56scupfqhlgyg5",
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
71
packages/sdk38/src/pubkey.ts
Normal file
71
packages/sdk38/src/pubkey.ts
Normal file
@ -0,0 +1,71 @@
|
||||
import { Bech32, Encoding } from "@iov/encoding";
|
||||
import equal from "fast-deep-equal";
|
||||
|
||||
import { PubKey, pubkeyType } from "./types";
|
||||
|
||||
export function encodeSecp256k1Pubkey(pubkey: Uint8Array): PubKey {
|
||||
if (pubkey.length !== 33 || (pubkey[0] !== 0x02 && pubkey[0] !== 0x03)) {
|
||||
throw new Error("Public key must be compressed secp256k1, i.e. 33 bytes starting with 0x02 or 0x03");
|
||||
}
|
||||
return {
|
||||
type: pubkeyType.secp256k1,
|
||||
value: Encoding.toBase64(pubkey),
|
||||
};
|
||||
}
|
||||
|
||||
// As discussed in https://github.com/binance-chain/javascript-sdk/issues/163
|
||||
// Prefixes listed here: https://github.com/tendermint/tendermint/blob/d419fffe18531317c28c29a292ad7d253f6cafdf/docs/spec/blockchain/encoding.md#public-key-cryptography
|
||||
// Last bytes is varint-encoded length prefix
|
||||
const pubkeyAminoPrefixSecp256k1 = Encoding.fromHex("eb5ae98721");
|
||||
const pubkeyAminoPrefixEd25519 = Encoding.fromHex("1624de6420");
|
||||
const pubkeyAminoPrefixSr25519 = Encoding.fromHex("0dfb1005");
|
||||
const pubkeyAminoPrefixLength = pubkeyAminoPrefixSecp256k1.length;
|
||||
|
||||
export function decodeBech32Pubkey(bechEncoded: string): PubKey {
|
||||
const { data } = Bech32.decode(bechEncoded);
|
||||
|
||||
const aminoPrefix = data.slice(0, pubkeyAminoPrefixLength);
|
||||
const rest = data.slice(pubkeyAminoPrefixLength);
|
||||
if (equal(aminoPrefix, pubkeyAminoPrefixSecp256k1)) {
|
||||
if (rest.length !== 33) {
|
||||
throw new Error("Invalid rest data length. Expected 33 bytes (compressed secp256k1 pubkey).");
|
||||
}
|
||||
return {
|
||||
type: pubkeyType.secp256k1,
|
||||
value: Encoding.toBase64(rest),
|
||||
};
|
||||
} else if (equal(aminoPrefix, pubkeyAminoPrefixEd25519)) {
|
||||
if (rest.length !== 32) {
|
||||
throw new Error("Invalid rest data length. Expected 32 bytes (Ed25519 pubkey).");
|
||||
}
|
||||
return {
|
||||
type: pubkeyType.ed25519,
|
||||
value: Encoding.toBase64(rest),
|
||||
};
|
||||
} else if (equal(aminoPrefix, pubkeyAminoPrefixSr25519)) {
|
||||
if (rest.length !== 32) {
|
||||
throw new Error("Invalid rest data length. Expected 32 bytes (Sr25519 pubkey).");
|
||||
}
|
||||
return {
|
||||
type: pubkeyType.sr25519,
|
||||
value: Encoding.toBase64(rest),
|
||||
};
|
||||
} else {
|
||||
throw new Error("Unsupported Pubkey type. Amino prefix: " + Encoding.toHex(aminoPrefix));
|
||||
}
|
||||
}
|
||||
|
||||
export function encodeBech32Pubkey(pubkey: PubKey, prefix: string): string {
|
||||
let aminoPrefix: Uint8Array;
|
||||
switch (pubkey.type) {
|
||||
// Note: please don't add cases here without writing additional unit tests
|
||||
case pubkeyType.secp256k1:
|
||||
aminoPrefix = pubkeyAminoPrefixSecp256k1;
|
||||
break;
|
||||
default:
|
||||
throw new Error("Unsupported pubkey type");
|
||||
}
|
||||
|
||||
const data = new Uint8Array([...aminoPrefix, ...Encoding.fromBase64(pubkey.value)]);
|
||||
return Bech32.encode(prefix, data);
|
||||
}
|
||||
897
packages/sdk38/src/restclient.spec.ts
Normal file
897
packages/sdk38/src/restclient.spec.ts
Normal file
@ -0,0 +1,897 @@
|
||||
/* eslint-disable @typescript-eslint/camelcase */
|
||||
import { Encoding } from "@iov/encoding";
|
||||
import { assert, sleep } from "@iov/utils";
|
||||
import { ReadonlyDate } from "readonly-date";
|
||||
|
||||
import { rawSecp256k1PubkeyToAddress } from "./address";
|
||||
import { makeSignBytes } from "./encoding";
|
||||
import { parseLogs } from "./logs";
|
||||
import { makeCosmoshubPath, Secp256k1Pen } from "./pen";
|
||||
import { encodeBech32Pubkey } from "./pubkey";
|
||||
import { RestClient, TxsResponse } from "./restclient";
|
||||
import { SigningCosmosClient } from "./signingcosmosclient";
|
||||
import cosmoshub from "./testdata/cosmoshub.json";
|
||||
import {
|
||||
faucet,
|
||||
makeRandomAddress,
|
||||
nonNegativeIntegerMatcher,
|
||||
pendingWithoutWasmd,
|
||||
semverMatcher,
|
||||
tendermintAddressMatcher,
|
||||
tendermintIdMatcher,
|
||||
tendermintOptionalIdMatcher,
|
||||
tendermintShortHashMatcher,
|
||||
unused,
|
||||
wasmd,
|
||||
wasmdEnabled,
|
||||
} from "./testutils.spec";
|
||||
import { Msg, MsgSend, StdFee, StdSignature, StdTx } from "./types";
|
||||
|
||||
const { fromBase64 } = Encoding;
|
||||
|
||||
const emptyAddress = "cosmos1ltkhnmdcqemmd2tkhnx7qx66tq7e0wykw2j85k";
|
||||
|
||||
function makeSignedTx(firstMsg: Msg, fee: StdFee, memo: string, firstSignature: StdSignature): StdTx {
|
||||
return {
|
||||
msg: [firstMsg],
|
||||
fee: fee,
|
||||
memo: memo,
|
||||
signatures: [firstSignature],
|
||||
};
|
||||
}
|
||||
|
||||
describe("RestClient", () => {
|
||||
it("can be constructed", () => {
|
||||
const client = new RestClient(wasmd.endpoint);
|
||||
expect(client).toBeTruthy();
|
||||
});
|
||||
|
||||
// The /auth endpoints
|
||||
|
||||
describe("authAccounts", () => {
|
||||
it("works for unused account without pubkey", async () => {
|
||||
pendingWithoutWasmd();
|
||||
const client = new RestClient(wasmd.endpoint);
|
||||
const { height, result } = await client.authAccounts(unused.address);
|
||||
expect(height).toMatch(nonNegativeIntegerMatcher);
|
||||
expect(result).toEqual({
|
||||
type: "cosmos-sdk/Account",
|
||||
value: {
|
||||
address: unused.address,
|
||||
public_key: "", // not known to the chain
|
||||
coins: [
|
||||
{
|
||||
amount: "1000000000",
|
||||
denom: "ucosm",
|
||||
},
|
||||
{
|
||||
amount: "1000000000",
|
||||
denom: "ustake",
|
||||
},
|
||||
],
|
||||
account_number: unused.accountNumber,
|
||||
sequence: 0,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
// This fails in the first test run if you forget to run `./scripts/wasmd/init.sh`
|
||||
it("has correct pubkey for faucet", async () => {
|
||||
pendingWithoutWasmd();
|
||||
const client = new RestClient(wasmd.endpoint);
|
||||
const { result } = await client.authAccounts(faucet.address);
|
||||
expect(result.value).toEqual(
|
||||
jasmine.objectContaining({
|
||||
public_key: encodeBech32Pubkey(faucet.pubkey, "cosmospub"),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
// This property is used by CosmWasmClient.getAccount
|
||||
it("returns empty address for non-existent account", async () => {
|
||||
pendingWithoutWasmd();
|
||||
const client = new RestClient(wasmd.endpoint);
|
||||
const nonExistentAccount = makeRandomAddress();
|
||||
const { result } = await client.authAccounts(nonExistentAccount);
|
||||
expect(result).toEqual({
|
||||
type: "cosmos-sdk/Account",
|
||||
value: jasmine.objectContaining({ address: "" }),
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// The /blocks endpoints
|
||||
|
||||
describe("blocksLatest", () => {
|
||||
it("works", async () => {
|
||||
pendingWithoutWasmd();
|
||||
const client = new RestClient(wasmd.endpoint);
|
||||
const response = await client.blocksLatest();
|
||||
|
||||
// id
|
||||
expect(response.block_id.hash).toMatch(tendermintIdMatcher);
|
||||
|
||||
// header
|
||||
expect(response.block.header.version).toEqual({ block: "10", app: "0" });
|
||||
expect(parseInt(response.block.header.height, 10)).toBeGreaterThanOrEqual(1);
|
||||
expect(response.block.header.chain_id).toEqual(wasmd.chainId);
|
||||
expect(new ReadonlyDate(response.block.header.time).getTime()).toBeLessThan(ReadonlyDate.now());
|
||||
expect(new ReadonlyDate(response.block.header.time).getTime()).toBeGreaterThanOrEqual(
|
||||
ReadonlyDate.now() - 5_000,
|
||||
);
|
||||
expect(response.block.header.last_commit_hash).toMatch(tendermintIdMatcher);
|
||||
expect(response.block.header.last_block_id.hash).toMatch(tendermintIdMatcher);
|
||||
expect(response.block.header.data_hash).toMatch(tendermintOptionalIdMatcher);
|
||||
expect(response.block.header.validators_hash).toMatch(tendermintIdMatcher);
|
||||
expect(response.block.header.next_validators_hash).toMatch(tendermintIdMatcher);
|
||||
expect(response.block.header.consensus_hash).toMatch(tendermintIdMatcher);
|
||||
expect(response.block.header.app_hash).toMatch(tendermintIdMatcher);
|
||||
expect(response.block.header.last_results_hash).toMatch(tendermintOptionalIdMatcher);
|
||||
expect(response.block.header.evidence_hash).toMatch(tendermintOptionalIdMatcher);
|
||||
expect(response.block.header.proposer_address).toMatch(tendermintAddressMatcher);
|
||||
|
||||
// data
|
||||
expect(response.block.data.txs === null || Array.isArray(response.block.data.txs)).toEqual(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("blocks", () => {
|
||||
it("works for block by height", async () => {
|
||||
pendingWithoutWasmd();
|
||||
const client = new RestClient(wasmd.endpoint);
|
||||
const height = parseInt((await client.blocksLatest()).block.header.height, 10);
|
||||
const response = await client.blocks(height - 1);
|
||||
|
||||
// id
|
||||
expect(response.block_id.hash).toMatch(tendermintIdMatcher);
|
||||
|
||||
// header
|
||||
expect(response.block.header.version).toEqual({ block: "10", app: "0" });
|
||||
expect(response.block.header.height).toEqual(`${height - 1}`);
|
||||
expect(response.block.header.chain_id).toEqual(wasmd.chainId);
|
||||
expect(new ReadonlyDate(response.block.header.time).getTime()).toBeLessThan(ReadonlyDate.now());
|
||||
expect(new ReadonlyDate(response.block.header.time).getTime()).toBeGreaterThanOrEqual(
|
||||
ReadonlyDate.now() - 5_000,
|
||||
);
|
||||
expect(response.block.header.last_commit_hash).toMatch(tendermintIdMatcher);
|
||||
expect(response.block.header.last_block_id.hash).toMatch(tendermintIdMatcher);
|
||||
expect(response.block.header.data_hash).toMatch(tendermintOptionalIdMatcher);
|
||||
expect(response.block.header.validators_hash).toMatch(tendermintIdMatcher);
|
||||
expect(response.block.header.next_validators_hash).toMatch(tendermintIdMatcher);
|
||||
expect(response.block.header.consensus_hash).toMatch(tendermintIdMatcher);
|
||||
expect(response.block.header.app_hash).toMatch(tendermintIdMatcher);
|
||||
expect(response.block.header.last_results_hash).toMatch(tendermintOptionalIdMatcher);
|
||||
expect(response.block.header.evidence_hash).toMatch(tendermintOptionalIdMatcher);
|
||||
expect(response.block.header.proposer_address).toMatch(tendermintAddressMatcher);
|
||||
|
||||
// data
|
||||
expect(response.block.data.txs === null || Array.isArray(response.block.data.txs)).toEqual(true);
|
||||
});
|
||||
});
|
||||
|
||||
// The /node_info endpoint
|
||||
|
||||
describe("nodeInfo", () => {
|
||||
it("works", async () => {
|
||||
pendingWithoutWasmd();
|
||||
const client = new RestClient(wasmd.endpoint);
|
||||
const { node_info, application_version } = await client.nodeInfo();
|
||||
|
||||
expect(node_info).toEqual({
|
||||
protocol_version: { p2p: "7", block: "10", app: "0" },
|
||||
id: jasmine.stringMatching(tendermintShortHashMatcher),
|
||||
listen_addr: "tcp://0.0.0.0:26656",
|
||||
network: wasmd.chainId,
|
||||
version: jasmine.stringMatching(/^0\.33\.[0-9]+$/),
|
||||
channels: "4020212223303800",
|
||||
moniker: wasmd.chainId,
|
||||
other: { tx_index: "on", rpc_address: "tcp://0.0.0.0:26657" },
|
||||
});
|
||||
expect(application_version).toEqual({
|
||||
name: "wasm",
|
||||
server_name: "wasmd",
|
||||
client_name: "wasmcli",
|
||||
version: jasmine.stringMatching(semverMatcher),
|
||||
commit: jasmine.stringMatching(tendermintShortHashMatcher),
|
||||
build_tags: "netgo,ledger",
|
||||
go: jasmine.stringMatching(/^go version go1\.[0-9]+\.[0-9]+ linux\/amd64$/),
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// The /txs endpoints
|
||||
|
||||
describe("txById", () => {
|
||||
let successful:
|
||||
| {
|
||||
readonly sender: string;
|
||||
readonly recipient: string;
|
||||
readonly hash: string;
|
||||
}
|
||||
| undefined;
|
||||
let unsuccessful:
|
||||
| {
|
||||
readonly sender: string;
|
||||
readonly recipient: string;
|
||||
readonly hash: string;
|
||||
}
|
||||
| undefined;
|
||||
|
||||
beforeAll(async () => {
|
||||
if (wasmdEnabled()) {
|
||||
const pen = await Secp256k1Pen.fromMnemonic(faucet.mnemonic);
|
||||
const client = new SigningCosmosClient(wasmd.endpoint, faucet.address, (signBytes) =>
|
||||
pen.sign(signBytes),
|
||||
);
|
||||
|
||||
{
|
||||
const recipient = makeRandomAddress();
|
||||
const transferAmount = {
|
||||
denom: "ucosm",
|
||||
amount: "1234567",
|
||||
};
|
||||
const result = await client.sendTokens(recipient, [transferAmount]);
|
||||
successful = {
|
||||
sender: faucet.address,
|
||||
recipient: recipient,
|
||||
hash: result.transactionHash,
|
||||
};
|
||||
}
|
||||
|
||||
{
|
||||
const memo = "Sending more than I can afford";
|
||||
const recipient = makeRandomAddress();
|
||||
const transferAmount = [
|
||||
{
|
||||
denom: "ucosm",
|
||||
amount: "123456700000000",
|
||||
},
|
||||
];
|
||||
const sendMsg: MsgSend = {
|
||||
type: "cosmos-sdk/MsgSend",
|
||||
value: {
|
||||
// eslint-disable-next-line @typescript-eslint/camelcase
|
||||
from_address: faucet.address,
|
||||
// eslint-disable-next-line @typescript-eslint/camelcase
|
||||
to_address: recipient,
|
||||
amount: transferAmount,
|
||||
},
|
||||
};
|
||||
const fee = {
|
||||
amount: [
|
||||
{
|
||||
denom: "ucosm",
|
||||
amount: "2000",
|
||||
},
|
||||
],
|
||||
gas: "80000", // 80k
|
||||
};
|
||||
const { accountNumber, sequence } = await client.getNonce();
|
||||
const chainId = await client.getChainId();
|
||||
const signBytes = makeSignBytes([sendMsg], fee, chainId, memo, accountNumber, sequence);
|
||||
const signature = await pen.sign(signBytes);
|
||||
const signedTx = {
|
||||
msg: [sendMsg],
|
||||
fee: fee,
|
||||
memo: memo,
|
||||
signatures: [signature],
|
||||
};
|
||||
const transactionId = await client.getIdentifier({ type: "cosmos-sdk/StdTx", value: signedTx });
|
||||
try {
|
||||
await client.postTx(signedTx);
|
||||
} catch (error) {
|
||||
// postTx() throws on execution failures, which is a questionable design. Ignore for now.
|
||||
// console.log(error);
|
||||
}
|
||||
unsuccessful = {
|
||||
sender: faucet.address,
|
||||
recipient: recipient,
|
||||
hash: transactionId,
|
||||
};
|
||||
}
|
||||
|
||||
await sleep(50); // wait until transactions are indexed
|
||||
}
|
||||
});
|
||||
|
||||
it("works for successful transaction", async () => {
|
||||
pendingWithoutWasmd();
|
||||
assert(successful);
|
||||
const client = new RestClient(wasmd.endpoint);
|
||||
const result = await client.txById(successful.hash);
|
||||
expect(result.height).toBeGreaterThanOrEqual(1);
|
||||
expect(result.txhash).toEqual(successful.hash);
|
||||
expect(result.codespace).toBeUndefined();
|
||||
expect(result.code).toBeUndefined();
|
||||
const logs = parseLogs(result.logs);
|
||||
expect(logs).toEqual([
|
||||
{
|
||||
msg_index: 0,
|
||||
log: "",
|
||||
events: [
|
||||
{
|
||||
type: "message",
|
||||
attributes: [
|
||||
{ key: "action", value: "send" },
|
||||
{ key: "sender", value: successful.sender },
|
||||
{ key: "module", value: "bank" },
|
||||
],
|
||||
},
|
||||
{
|
||||
type: "transfer",
|
||||
attributes: [
|
||||
{ key: "recipient", value: successful.recipient },
|
||||
{ key: "sender", value: successful.sender },
|
||||
{ key: "amount", value: "1234567ucosm" },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it("works for unsuccessful transaction", async () => {
|
||||
pendingWithoutWasmd();
|
||||
assert(unsuccessful);
|
||||
const client = new RestClient(wasmd.endpoint);
|
||||
const result = await client.txById(unsuccessful.hash);
|
||||
expect(result.height).toBeGreaterThanOrEqual(1);
|
||||
expect(result.txhash).toEqual(unsuccessful.hash);
|
||||
expect(result.codespace).toEqual("sdk");
|
||||
expect(result.code).toEqual(5);
|
||||
expect(result.logs).toBeUndefined();
|
||||
expect(result.raw_log).toContain("insufficient funds");
|
||||
});
|
||||
});
|
||||
|
||||
describe("txsQuery", () => {
|
||||
let posted:
|
||||
| {
|
||||
readonly sender: string;
|
||||
readonly recipient: string;
|
||||
readonly hash: string;
|
||||
readonly height: number;
|
||||
readonly tx: TxsResponse;
|
||||
}
|
||||
| undefined;
|
||||
|
||||
beforeAll(async () => {
|
||||
if (wasmdEnabled()) {
|
||||
const pen = await Secp256k1Pen.fromMnemonic(faucet.mnemonic);
|
||||
const client = new SigningCosmosClient(wasmd.endpoint, faucet.address, (signBytes) =>
|
||||
pen.sign(signBytes),
|
||||
);
|
||||
|
||||
const recipient = makeRandomAddress();
|
||||
const transferAmount = [
|
||||
{
|
||||
denom: "ucosm",
|
||||
amount: "1234567",
|
||||
},
|
||||
];
|
||||
const result = await client.sendTokens(recipient, transferAmount);
|
||||
|
||||
await sleep(50); // wait until tx is indexed
|
||||
const txDetails = await new RestClient(wasmd.endpoint).txById(result.transactionHash);
|
||||
posted = {
|
||||
sender: faucet.address,
|
||||
recipient: recipient,
|
||||
hash: result.transactionHash,
|
||||
height: Number.parseInt(txDetails.height, 10),
|
||||
tx: txDetails,
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
it("can query transactions by height", async () => {
|
||||
pendingWithoutWasmd();
|
||||
assert(posted);
|
||||
const client = new RestClient(wasmd.endpoint);
|
||||
const result = await client.txsQuery(`tx.height=${posted.height}&limit=26`);
|
||||
expect(result).toEqual({
|
||||
count: "1",
|
||||
limit: "26",
|
||||
page_number: "1",
|
||||
page_total: "1",
|
||||
total_count: "1",
|
||||
txs: [posted.tx],
|
||||
});
|
||||
});
|
||||
|
||||
it("can query transactions by ID", async () => {
|
||||
pendingWithoutWasmd();
|
||||
assert(posted);
|
||||
const client = new RestClient(wasmd.endpoint);
|
||||
const result = await client.txsQuery(`tx.hash=${posted.hash}&limit=26`);
|
||||
expect(result).toEqual({
|
||||
count: "1",
|
||||
limit: "26",
|
||||
page_number: "1",
|
||||
page_total: "1",
|
||||
total_count: "1",
|
||||
txs: [posted.tx],
|
||||
});
|
||||
});
|
||||
|
||||
it("can query transactions by sender", async () => {
|
||||
pendingWithoutWasmd();
|
||||
assert(posted);
|
||||
const client = new RestClient(wasmd.endpoint);
|
||||
const result = await client.txsQuery(`message.sender=${posted.sender}&limit=200`);
|
||||
expect(parseInt(result.count, 10)).toBeGreaterThanOrEqual(1);
|
||||
expect(parseInt(result.limit, 10)).toEqual(200);
|
||||
expect(parseInt(result.page_number, 10)).toEqual(1);
|
||||
expect(parseInt(result.page_total, 10)).toEqual(1);
|
||||
expect(parseInt(result.total_count, 10)).toBeGreaterThanOrEqual(1);
|
||||
expect(result.txs.length).toBeGreaterThanOrEqual(1);
|
||||
expect(result.txs[result.txs.length - 1]).toEqual(posted.tx);
|
||||
});
|
||||
|
||||
it("can query transactions by recipient", async () => {
|
||||
pendingWithoutWasmd();
|
||||
assert(posted);
|
||||
const client = new RestClient(wasmd.endpoint);
|
||||
const result = await client.txsQuery(`transfer.recipient=${posted.recipient}&limit=200`);
|
||||
expect(parseInt(result.count, 10)).toEqual(1);
|
||||
expect(parseInt(result.limit, 10)).toEqual(200);
|
||||
expect(parseInt(result.page_number, 10)).toEqual(1);
|
||||
expect(parseInt(result.page_total, 10)).toEqual(1);
|
||||
expect(parseInt(result.total_count, 10)).toEqual(1);
|
||||
expect(result.txs.length).toBeGreaterThanOrEqual(1);
|
||||
expect(result.txs[result.txs.length - 1]).toEqual(posted.tx);
|
||||
});
|
||||
|
||||
it("can filter by tx.hash and tx.minheight", async () => {
|
||||
pending("This combination is broken 🤷♂️. Handle client-side at higher level.");
|
||||
pendingWithoutWasmd();
|
||||
assert(posted);
|
||||
const client = new RestClient(wasmd.endpoint);
|
||||
const hashQuery = `tx.hash=${posted.hash}`;
|
||||
|
||||
{
|
||||
const { count } = await client.txsQuery(`${hashQuery}&tx.minheight=0`);
|
||||
expect(count).toEqual("1");
|
||||
}
|
||||
|
||||
{
|
||||
const { count } = await client.txsQuery(`${hashQuery}&tx.minheight=${posted.height - 1}`);
|
||||
expect(count).toEqual("1");
|
||||
}
|
||||
|
||||
{
|
||||
const { count } = await client.txsQuery(`${hashQuery}&tx.minheight=${posted.height}`);
|
||||
expect(count).toEqual("1");
|
||||
}
|
||||
|
||||
{
|
||||
const { count } = await client.txsQuery(`${hashQuery}&tx.minheight=${posted.height + 1}`);
|
||||
expect(count).toEqual("0");
|
||||
}
|
||||
});
|
||||
|
||||
it("can filter by recipient and tx.minheight", async () => {
|
||||
pendingWithoutWasmd();
|
||||
assert(posted);
|
||||
const client = new RestClient(wasmd.endpoint);
|
||||
const recipientQuery = `transfer.recipient=${posted.recipient}`;
|
||||
|
||||
{
|
||||
const { count } = await client.txsQuery(`${recipientQuery}&tx.minheight=0`);
|
||||
expect(count).toEqual("1");
|
||||
}
|
||||
|
||||
{
|
||||
const { count } = await client.txsQuery(`${recipientQuery}&tx.minheight=${posted.height - 1}`);
|
||||
expect(count).toEqual("1");
|
||||
}
|
||||
|
||||
{
|
||||
const { count } = await client.txsQuery(`${recipientQuery}&tx.minheight=${posted.height}`);
|
||||
expect(count).toEqual("1");
|
||||
}
|
||||
|
||||
{
|
||||
const { count } = await client.txsQuery(`${recipientQuery}&tx.minheight=${posted.height + 1}`);
|
||||
expect(count).toEqual("0");
|
||||
}
|
||||
});
|
||||
|
||||
it("can filter by recipient and tx.maxheight", async () => {
|
||||
pendingWithoutWasmd();
|
||||
assert(posted);
|
||||
const client = new RestClient(wasmd.endpoint);
|
||||
const recipientQuery = `transfer.recipient=${posted.recipient}`;
|
||||
|
||||
{
|
||||
const { count } = await client.txsQuery(`${recipientQuery}&tx.maxheight=9999999999999`);
|
||||
expect(count).toEqual("1");
|
||||
}
|
||||
|
||||
{
|
||||
const { count } = await client.txsQuery(`${recipientQuery}&tx.maxheight=${posted.height + 1}`);
|
||||
expect(count).toEqual("1");
|
||||
}
|
||||
|
||||
{
|
||||
const { count } = await client.txsQuery(`${recipientQuery}&tx.maxheight=${posted.height}`);
|
||||
expect(count).toEqual("1");
|
||||
}
|
||||
|
||||
{
|
||||
const { count } = await client.txsQuery(`${recipientQuery}&tx.maxheight=${posted.height - 1}`);
|
||||
expect(count).toEqual("0");
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe("encodeTx", () => {
|
||||
it("works for cosmoshub example", async () => {
|
||||
pendingWithoutWasmd();
|
||||
const client = new RestClient(wasmd.endpoint);
|
||||
expect(await client.encodeTx(cosmoshub.tx)).toEqual(fromBase64(cosmoshub.tx_data));
|
||||
});
|
||||
});
|
||||
|
||||
describe("postTx", () => {
|
||||
it("can send tokens", async () => {
|
||||
pendingWithoutWasmd();
|
||||
const pen = await Secp256k1Pen.fromMnemonic(faucet.mnemonic);
|
||||
|
||||
const memo = "My first contract on chain";
|
||||
const theMsg: MsgSend = {
|
||||
type: "cosmos-sdk/MsgSend",
|
||||
value: {
|
||||
from_address: faucet.address,
|
||||
to_address: emptyAddress,
|
||||
amount: [
|
||||
{
|
||||
denom: "ucosm",
|
||||
amount: "1234567",
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
const fee: StdFee = {
|
||||
amount: [
|
||||
{
|
||||
amount: "5000",
|
||||
denom: "ucosm",
|
||||
},
|
||||
],
|
||||
gas: "890000",
|
||||
};
|
||||
|
||||
const client = new RestClient(wasmd.endpoint);
|
||||
const { account_number, sequence } = (await client.authAccounts(faucet.address)).result.value;
|
||||
|
||||
const signBytes = makeSignBytes([theMsg], fee, wasmd.chainId, memo, account_number, sequence);
|
||||
const signature = await pen.sign(signBytes);
|
||||
const signedTx = makeSignedTx(theMsg, fee, memo, signature);
|
||||
const result = await client.postTx(signedTx);
|
||||
expect(result.code).toBeUndefined();
|
||||
expect(result).toEqual({
|
||||
height: jasmine.stringMatching(nonNegativeIntegerMatcher),
|
||||
txhash: jasmine.stringMatching(tendermintIdMatcher),
|
||||
// code is not set
|
||||
raw_log: jasmine.stringMatching(/^\[.+\]$/i),
|
||||
logs: jasmine.any(Array),
|
||||
gas_wanted: jasmine.stringMatching(nonNegativeIntegerMatcher),
|
||||
gas_used: jasmine.stringMatching(nonNegativeIntegerMatcher),
|
||||
});
|
||||
});
|
||||
|
||||
it("can't send transaction with additional signatures", async () => {
|
||||
pendingWithoutWasmd();
|
||||
const account1 = await Secp256k1Pen.fromMnemonic(faucet.mnemonic, makeCosmoshubPath(0));
|
||||
const account2 = await Secp256k1Pen.fromMnemonic(faucet.mnemonic, makeCosmoshubPath(1));
|
||||
const account3 = await Secp256k1Pen.fromMnemonic(faucet.mnemonic, makeCosmoshubPath(2));
|
||||
const address1 = rawSecp256k1PubkeyToAddress(account1.pubkey, "cosmos");
|
||||
const address2 = rawSecp256k1PubkeyToAddress(account2.pubkey, "cosmos");
|
||||
const address3 = rawSecp256k1PubkeyToAddress(account3.pubkey, "cosmos");
|
||||
|
||||
const memo = "My first contract on chain";
|
||||
const theMsg: MsgSend = {
|
||||
type: "cosmos-sdk/MsgSend",
|
||||
value: {
|
||||
from_address: address1,
|
||||
to_address: emptyAddress,
|
||||
amount: [
|
||||
{
|
||||
denom: "ucosm",
|
||||
amount: "1234567",
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
const fee: StdFee = {
|
||||
amount: [
|
||||
{
|
||||
amount: "5000",
|
||||
denom: "ucosm",
|
||||
},
|
||||
],
|
||||
gas: "890000",
|
||||
};
|
||||
|
||||
const client = new RestClient(wasmd.endpoint);
|
||||
const { account_number: an1, sequence: sequence1 } = (await client.authAccounts(address1)).result.value;
|
||||
const { account_number: an2, sequence: sequence2 } = (await client.authAccounts(address2)).result.value;
|
||||
const { account_number: an3, sequence: sequence3 } = (await client.authAccounts(address3)).result.value;
|
||||
|
||||
const signBytes1 = makeSignBytes([theMsg], fee, wasmd.chainId, memo, an1, sequence1);
|
||||
const signBytes2 = makeSignBytes([theMsg], fee, wasmd.chainId, memo, an2, sequence2);
|
||||
const signBytes3 = makeSignBytes([theMsg], fee, wasmd.chainId, memo, an3, sequence3);
|
||||
const signature1 = await account1.sign(signBytes1);
|
||||
const signature2 = await account2.sign(signBytes2);
|
||||
const signature3 = await account3.sign(signBytes3);
|
||||
const signedTx = {
|
||||
msg: [theMsg],
|
||||
fee: fee,
|
||||
memo: memo,
|
||||
signatures: [signature1, signature2, signature3],
|
||||
};
|
||||
const postResult = await client.postTx(signedTx);
|
||||
// console.log(postResult.raw_log);
|
||||
expect(postResult.code).toEqual(4);
|
||||
expect(postResult.raw_log).toContain("wrong number of signers");
|
||||
});
|
||||
|
||||
it("can send multiple messages with one signature", async () => {
|
||||
pendingWithoutWasmd();
|
||||
const account1 = await Secp256k1Pen.fromMnemonic(faucet.mnemonic, makeCosmoshubPath(0));
|
||||
const address1 = rawSecp256k1PubkeyToAddress(account1.pubkey, "cosmos");
|
||||
|
||||
const memo = "My first contract on chain";
|
||||
const msg1: MsgSend = {
|
||||
type: "cosmos-sdk/MsgSend",
|
||||
value: {
|
||||
from_address: address1,
|
||||
to_address: emptyAddress,
|
||||
amount: [
|
||||
{
|
||||
denom: "ucosm",
|
||||
amount: "1234567",
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
const msg2: MsgSend = {
|
||||
type: "cosmos-sdk/MsgSend",
|
||||
value: {
|
||||
from_address: address1,
|
||||
to_address: emptyAddress,
|
||||
amount: [
|
||||
{
|
||||
denom: "ucosm",
|
||||
amount: "7654321",
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
const fee: StdFee = {
|
||||
amount: [
|
||||
{
|
||||
amount: "5000",
|
||||
denom: "ucosm",
|
||||
},
|
||||
],
|
||||
gas: "890000",
|
||||
};
|
||||
|
||||
const client = new RestClient(wasmd.endpoint);
|
||||
const { account_number, sequence } = (await client.authAccounts(address1)).result.value;
|
||||
|
||||
const signBytes = makeSignBytes([msg1, msg2], fee, wasmd.chainId, memo, account_number, sequence);
|
||||
const signature1 = await account1.sign(signBytes);
|
||||
const signedTx = {
|
||||
msg: [msg1, msg2],
|
||||
fee: fee,
|
||||
memo: memo,
|
||||
signatures: [signature1],
|
||||
};
|
||||
const postResult = await client.postTx(signedTx);
|
||||
// console.log(postResult.raw_log);
|
||||
expect(postResult.code).toBeUndefined();
|
||||
});
|
||||
|
||||
it("can send multiple messages with multiple signatures", async () => {
|
||||
pendingWithoutWasmd();
|
||||
const account1 = await Secp256k1Pen.fromMnemonic(faucet.mnemonic, makeCosmoshubPath(0));
|
||||
const account2 = await Secp256k1Pen.fromMnemonic(faucet.mnemonic, makeCosmoshubPath(1));
|
||||
const address1 = rawSecp256k1PubkeyToAddress(account1.pubkey, "cosmos");
|
||||
const address2 = rawSecp256k1PubkeyToAddress(account2.pubkey, "cosmos");
|
||||
|
||||
const memo = "My first contract on chain";
|
||||
const msg1: MsgSend = {
|
||||
type: "cosmos-sdk/MsgSend",
|
||||
value: {
|
||||
from_address: address1,
|
||||
to_address: emptyAddress,
|
||||
amount: [
|
||||
{
|
||||
denom: "ucosm",
|
||||
amount: "1234567",
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
const msg2: MsgSend = {
|
||||
type: "cosmos-sdk/MsgSend",
|
||||
value: {
|
||||
from_address: address2,
|
||||
to_address: emptyAddress,
|
||||
amount: [
|
||||
{
|
||||
denom: "ucosm",
|
||||
amount: "7654321",
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
const fee: StdFee = {
|
||||
amount: [
|
||||
{
|
||||
amount: "5000",
|
||||
denom: "ucosm",
|
||||
},
|
||||
],
|
||||
gas: "890000",
|
||||
};
|
||||
|
||||
const client = new RestClient(wasmd.endpoint);
|
||||
const { account_number: an1, sequence: sequence1 } = (await client.authAccounts(address1)).result.value;
|
||||
const { account_number: an2, sequence: sequence2 } = (await client.authAccounts(address2)).result.value;
|
||||
|
||||
const signBytes1 = makeSignBytes([msg2, msg1], fee, wasmd.chainId, memo, an1, sequence1);
|
||||
const signBytes2 = makeSignBytes([msg2, msg1], fee, wasmd.chainId, memo, an2, sequence2);
|
||||
const signature1 = await account1.sign(signBytes1);
|
||||
const signature2 = await account2.sign(signBytes2);
|
||||
const signedTx = {
|
||||
msg: [msg2, msg1],
|
||||
fee: fee,
|
||||
memo: memo,
|
||||
signatures: [signature2, signature1],
|
||||
};
|
||||
const postResult = await client.postTx(signedTx);
|
||||
// console.log(postResult.raw_log);
|
||||
expect(postResult.code).toBeUndefined();
|
||||
|
||||
await sleep(500);
|
||||
const searched = await client.txsQuery(`tx.hash=${postResult.txhash}`);
|
||||
expect(searched.txs.length).toEqual(1);
|
||||
expect(searched.txs[0].tx.value.signatures).toEqual([signature2, signature1]);
|
||||
});
|
||||
|
||||
it("can't send transaction with wrong signature order (1)", async () => {
|
||||
pendingWithoutWasmd();
|
||||
const account1 = await Secp256k1Pen.fromMnemonic(faucet.mnemonic, makeCosmoshubPath(0));
|
||||
const account2 = await Secp256k1Pen.fromMnemonic(faucet.mnemonic, makeCosmoshubPath(1));
|
||||
const address1 = rawSecp256k1PubkeyToAddress(account1.pubkey, "cosmos");
|
||||
const address2 = rawSecp256k1PubkeyToAddress(account2.pubkey, "cosmos");
|
||||
|
||||
const memo = "My first contract on chain";
|
||||
const msg1: MsgSend = {
|
||||
type: "cosmos-sdk/MsgSend",
|
||||
value: {
|
||||
from_address: address1,
|
||||
to_address: emptyAddress,
|
||||
amount: [
|
||||
{
|
||||
denom: "ucosm",
|
||||
amount: "1234567",
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
const msg2: MsgSend = {
|
||||
type: "cosmos-sdk/MsgSend",
|
||||
value: {
|
||||
from_address: address2,
|
||||
to_address: emptyAddress,
|
||||
amount: [
|
||||
{
|
||||
denom: "ucosm",
|
||||
amount: "7654321",
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
const fee: StdFee = {
|
||||
amount: [
|
||||
{
|
||||
amount: "5000",
|
||||
denom: "ucosm",
|
||||
},
|
||||
],
|
||||
gas: "890000",
|
||||
};
|
||||
|
||||
const client = new RestClient(wasmd.endpoint);
|
||||
const { account_number: an1, sequence: sequence1 } = (await client.authAccounts(address1)).result.value;
|
||||
const { account_number: an2, sequence: sequence2 } = (await client.authAccounts(address2)).result.value;
|
||||
|
||||
const signBytes1 = makeSignBytes([msg1, msg2], fee, wasmd.chainId, memo, an1, sequence1);
|
||||
const signBytes2 = makeSignBytes([msg1, msg2], fee, wasmd.chainId, memo, an2, sequence2);
|
||||
const signature1 = await account1.sign(signBytes1);
|
||||
const signature2 = await account2.sign(signBytes2);
|
||||
const signedTx = {
|
||||
msg: [msg1, msg2],
|
||||
fee: fee,
|
||||
memo: memo,
|
||||
signatures: [signature2, signature1],
|
||||
};
|
||||
const postResult = await client.postTx(signedTx);
|
||||
// console.log(postResult.raw_log);
|
||||
expect(postResult.code).toEqual(8);
|
||||
});
|
||||
|
||||
it("can't send transaction with wrong signature order (2)", async () => {
|
||||
pendingWithoutWasmd();
|
||||
const account1 = await Secp256k1Pen.fromMnemonic(faucet.mnemonic, makeCosmoshubPath(0));
|
||||
const account2 = await Secp256k1Pen.fromMnemonic(faucet.mnemonic, makeCosmoshubPath(1));
|
||||
const address1 = rawSecp256k1PubkeyToAddress(account1.pubkey, "cosmos");
|
||||
const address2 = rawSecp256k1PubkeyToAddress(account2.pubkey, "cosmos");
|
||||
|
||||
const memo = "My first contract on chain";
|
||||
const msg1: MsgSend = {
|
||||
type: "cosmos-sdk/MsgSend",
|
||||
value: {
|
||||
from_address: address1,
|
||||
to_address: emptyAddress,
|
||||
amount: [
|
||||
{
|
||||
denom: "ucosm",
|
||||
amount: "1234567",
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
const msg2: MsgSend = {
|
||||
type: "cosmos-sdk/MsgSend",
|
||||
value: {
|
||||
from_address: address2,
|
||||
to_address: emptyAddress,
|
||||
amount: [
|
||||
{
|
||||
denom: "ucosm",
|
||||
amount: "7654321",
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
const fee: StdFee = {
|
||||
amount: [
|
||||
{
|
||||
amount: "5000",
|
||||
denom: "ucosm",
|
||||
},
|
||||
],
|
||||
gas: "890000",
|
||||
};
|
||||
|
||||
const client = new RestClient(wasmd.endpoint);
|
||||
const { account_number: an1, sequence: sequence1 } = (await client.authAccounts(address1)).result.value;
|
||||
const { account_number: an2, sequence: sequence2 } = (await client.authAccounts(address2)).result.value;
|
||||
|
||||
const signBytes1 = makeSignBytes([msg2, msg1], fee, wasmd.chainId, memo, an1, sequence1);
|
||||
const signBytes2 = makeSignBytes([msg2, msg1], fee, wasmd.chainId, memo, an2, sequence2);
|
||||
const signature1 = await account1.sign(signBytes1);
|
||||
const signature2 = await account2.sign(signBytes2);
|
||||
const signedTx = {
|
||||
msg: [msg2, msg1],
|
||||
fee: fee,
|
||||
memo: memo,
|
||||
signatures: [signature1, signature2],
|
||||
};
|
||||
const postResult = await client.postTx(signedTx);
|
||||
// console.log(postResult.raw_log);
|
||||
expect(postResult.code).toEqual(8);
|
||||
});
|
||||
});
|
||||
});
|
||||
457
packages/sdk38/src/restclient.ts
Normal file
457
packages/sdk38/src/restclient.ts
Normal file
@ -0,0 +1,457 @@
|
||||
import { Encoding, isNonNullObject } from "@iov/encoding";
|
||||
import axios, { AxiosError, AxiosInstance } from "axios";
|
||||
|
||||
import { Coin } from "./coins";
|
||||
import { CosmosSdkTx, JsonObject, Model, parseWasmData, StdTx, WasmData } from "./types";
|
||||
|
||||
const { fromBase64, fromUtf8, toHex, toUtf8 } = Encoding;
|
||||
|
||||
export interface CosmosSdkAccount {
|
||||
/** Bech32 account address */
|
||||
readonly address: string;
|
||||
readonly coins: ReadonlyArray<Coin>;
|
||||
/** Bech32 encoded pubkey */
|
||||
readonly public_key: string;
|
||||
readonly account_number: number;
|
||||
readonly sequence: number;
|
||||
}
|
||||
|
||||
export interface NodeInfo {
|
||||
readonly protocol_version: {
|
||||
readonly p2p: string;
|
||||
readonly block: string;
|
||||
readonly app: string;
|
||||
};
|
||||
readonly id: string;
|
||||
readonly listen_addr: string;
|
||||
readonly network: string;
|
||||
readonly version: string;
|
||||
readonly channels: string;
|
||||
readonly moniker: string;
|
||||
readonly other: {
|
||||
readonly tx_index: string;
|
||||
readonly rpc_address: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface ApplicationVersion {
|
||||
readonly name: string;
|
||||
readonly server_name: string;
|
||||
readonly client_name: string;
|
||||
readonly version: string;
|
||||
readonly commit: string;
|
||||
readonly build_tags: string;
|
||||
readonly go: string;
|
||||
}
|
||||
|
||||
export interface NodeInfoResponse {
|
||||
readonly node_info: NodeInfo;
|
||||
readonly application_version: ApplicationVersion;
|
||||
}
|
||||
|
||||
export interface BlockId {
|
||||
readonly hash: string;
|
||||
// TODO: here we also have this
|
||||
// parts: {
|
||||
// total: '1',
|
||||
// hash: '7AF200C78FBF9236944E1AB270F4045CD60972B7C265E3A9DA42973397572931'
|
||||
// }
|
||||
}
|
||||
|
||||
export interface BlockHeader {
|
||||
readonly version: {
|
||||
readonly block: string;
|
||||
readonly app: string;
|
||||
};
|
||||
readonly height: string;
|
||||
readonly chain_id: string;
|
||||
/** An RFC 3339 time string like e.g. '2020-02-15T10:39:10.4696305Z' */
|
||||
readonly time: string;
|
||||
readonly last_commit_hash: string;
|
||||
readonly last_block_id: BlockId;
|
||||
/** Can be empty */
|
||||
readonly data_hash: string;
|
||||
readonly validators_hash: string;
|
||||
readonly next_validators_hash: string;
|
||||
readonly consensus_hash: string;
|
||||
readonly app_hash: string;
|
||||
/** Can be empty */
|
||||
readonly last_results_hash: string;
|
||||
/** Can be empty */
|
||||
readonly evidence_hash: string;
|
||||
readonly proposer_address: string;
|
||||
}
|
||||
|
||||
export interface Block {
|
||||
readonly header: BlockHeader;
|
||||
readonly data: {
|
||||
/** Array of base64 encoded transactions */
|
||||
readonly txs: ReadonlyArray<string> | null;
|
||||
};
|
||||
}
|
||||
|
||||
export interface BlockResponse {
|
||||
readonly block_id: BlockId;
|
||||
readonly block: Block;
|
||||
}
|
||||
|
||||
interface AuthAccountsResponse {
|
||||
readonly height: string;
|
||||
readonly result: {
|
||||
readonly type: "cosmos-sdk/Account";
|
||||
readonly value: CosmosSdkAccount;
|
||||
};
|
||||
}
|
||||
|
||||
// Currently all wasm query responses return json-encoded strings...
|
||||
// later deprecate this and use the specific types for result
|
||||
// (assuming it is inlined, no second parse needed)
|
||||
type WasmResponse<T = string> = WasmSuccess<T> | WasmError;
|
||||
|
||||
interface WasmSuccess<T = string> {
|
||||
readonly height: string;
|
||||
readonly result: T;
|
||||
}
|
||||
|
||||
interface WasmError {
|
||||
readonly error: string;
|
||||
}
|
||||
|
||||
export interface TxsResponse {
|
||||
readonly height: string;
|
||||
readonly txhash: string;
|
||||
/** 🤷♂️ */
|
||||
readonly codespace?: string;
|
||||
/** Falsy when transaction execution succeeded. Contains error code on error. */
|
||||
readonly code?: number;
|
||||
readonly raw_log: string;
|
||||
readonly logs?: object;
|
||||
readonly tx: CosmosSdkTx;
|
||||
/** The gas limit as set by the user */
|
||||
readonly gas_wanted?: string;
|
||||
/** The gas used by the execution */
|
||||
readonly gas_used?: string;
|
||||
readonly timestamp: string;
|
||||
}
|
||||
|
||||
interface SearchTxsResponse {
|
||||
readonly total_count: string;
|
||||
readonly count: string;
|
||||
readonly page_number: string;
|
||||
readonly page_total: string;
|
||||
readonly limit: string;
|
||||
readonly txs: readonly TxsResponse[];
|
||||
}
|
||||
|
||||
export interface PostTxsResponse {
|
||||
readonly height: string;
|
||||
readonly txhash: string;
|
||||
readonly code?: number;
|
||||
readonly raw_log?: string;
|
||||
/** The same as `raw_log` but deserialized? */
|
||||
readonly logs?: object;
|
||||
/** The gas limit as set by the user */
|
||||
readonly gas_wanted?: string;
|
||||
/** The gas used by the execution */
|
||||
readonly gas_used?: string;
|
||||
}
|
||||
|
||||
interface EncodeTxResponse {
|
||||
// base64-encoded amino-binary encoded representation
|
||||
readonly tx: string;
|
||||
}
|
||||
|
||||
export interface CodeInfo {
|
||||
readonly id: number;
|
||||
/** Bech32 account address */
|
||||
readonly creator: string;
|
||||
/** Hex-encoded sha256 hash of the code stored here */
|
||||
readonly data_hash: string;
|
||||
// TODO: these are not supported in current wasmd
|
||||
readonly source?: string;
|
||||
readonly builder?: string;
|
||||
}
|
||||
|
||||
export interface CodeDetails extends CodeInfo {
|
||||
/** Base64 encoded raw wasm data */
|
||||
readonly data: string;
|
||||
}
|
||||
|
||||
// This is list view, without contract info
|
||||
export interface ContractInfo {
|
||||
readonly address: string;
|
||||
readonly code_id: number;
|
||||
/** Bech32 account address */
|
||||
readonly creator: string;
|
||||
readonly label: string;
|
||||
}
|
||||
|
||||
export interface ContractDetails extends ContractInfo {
|
||||
/** Argument passed on initialization of the contract */
|
||||
readonly init_msg: object;
|
||||
}
|
||||
|
||||
interface SmartQueryResponse {
|
||||
// base64 encoded response
|
||||
readonly smart: string;
|
||||
}
|
||||
|
||||
type RestClientResponse =
|
||||
| NodeInfoResponse
|
||||
| BlockResponse
|
||||
| AuthAccountsResponse
|
||||
| TxsResponse
|
||||
| SearchTxsResponse
|
||||
| PostTxsResponse
|
||||
| EncodeTxResponse
|
||||
| WasmResponse<string>
|
||||
| WasmResponse<CodeInfo[]>
|
||||
| WasmResponse<CodeDetails>
|
||||
| WasmResponse<ContractInfo[] | null>
|
||||
| WasmResponse<ContractDetails | null>;
|
||||
|
||||
/** Unfortunately, Cosmos SDK encodes empty arrays as null */
|
||||
type CosmosSdkArray<T> = ReadonlyArray<T> | null;
|
||||
|
||||
function normalizeArray<T>(backend: CosmosSdkArray<T>): ReadonlyArray<T> {
|
||||
return backend || [];
|
||||
}
|
||||
|
||||
/**
|
||||
* The mode used to send transaction
|
||||
*
|
||||
* @see https://cosmos.network/rpc/#/Transactions/post_txs
|
||||
*/
|
||||
export enum BroadcastMode {
|
||||
/** Return after tx commit */
|
||||
Block = "block",
|
||||
/** Return afer CheckTx */
|
||||
Sync = "sync",
|
||||
/** Return right away */
|
||||
Async = "async",
|
||||
}
|
||||
|
||||
function isWasmError<T>(resp: WasmResponse<T>): resp is WasmError {
|
||||
return (resp as WasmError).error !== undefined;
|
||||
}
|
||||
|
||||
function unwrapWasmResponse<T>(response: WasmResponse<T>): T {
|
||||
if (isWasmError(response)) {
|
||||
throw new Error(response.error);
|
||||
}
|
||||
return response.result;
|
||||
}
|
||||
|
||||
// We want to get message data from 500 errors
|
||||
// https://stackoverflow.com/questions/56577124/how-to-handle-500-error-message-with-axios
|
||||
// this should be chained to catch one error and throw a more informative one
|
||||
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 RestClient {
|
||||
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<RestClientResponse> {
|
||||
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<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");
|
||||
}
|
||||
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 as any).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 as any).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 as any).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 as any).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 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) {
|
||||
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<Uint8Array> {
|
||||
const responseData = await this.post("/txs/encode", tx);
|
||||
if (!(responseData as any).tx) {
|
||||
throw new Error("Unexpected response data format");
|
||||
}
|
||||
return Encoding.fromBase64((responseData as EncodeTxResponse).tx);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 as any).txhash) {
|
||||
throw new Error("Unexpected response data format");
|
||||
}
|
||||
return responseData as PostTxsResponse;
|
||||
}
|
||||
|
||||
// The /wasm endpoints
|
||||
|
||||
// wasm rest queries are listed here: https://github.com/cosmwasm/wasmd/blob/master/x/wasm/client/rest/query.go#L19-L27
|
||||
public async listCodeInfo(): Promise<readonly CodeInfo[]> {
|
||||
const path = `/wasm/code`;
|
||||
const responseData = (await this.get(path)) as WasmResponse<CosmosSdkArray<CodeInfo>>;
|
||||
return normalizeArray(unwrapWasmResponse(responseData));
|
||||
}
|
||||
|
||||
// this will download the original wasm bytecode by code id
|
||||
// throws error if no code with this id
|
||||
public async getCode(id: number): Promise<CodeDetails> {
|
||||
const path = `/wasm/code/${id}`;
|
||||
const responseData = (await this.get(path)) as WasmResponse<CodeDetails>;
|
||||
return unwrapWasmResponse(responseData);
|
||||
}
|
||||
|
||||
public async listContractsByCodeId(id: number): Promise<readonly ContractInfo[]> {
|
||||
const path = `/wasm/code/${id}/contracts`;
|
||||
const responseData = (await this.get(path)) as WasmResponse<CosmosSdkArray<ContractInfo>>;
|
||||
return normalizeArray(unwrapWasmResponse(responseData));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns null when contract was not found at this address.
|
||||
*/
|
||||
public async getContractInfo(address: string): Promise<ContractDetails | null> {
|
||||
const path = `/wasm/contract/${address}`;
|
||||
const response = (await this.get(path)) as WasmResponse<ContractDetails | null>;
|
||||
return unwrapWasmResponse(response);
|
||||
}
|
||||
|
||||
// Returns all contract state.
|
||||
// This is an empty array if no such contract, or contract has no data.
|
||||
public async getAllContractState(address: string): Promise<readonly Model[]> {
|
||||
const path = `/wasm/contract/${address}/state`;
|
||||
const responseData = (await this.get(path)) as WasmResponse<CosmosSdkArray<WasmData>>;
|
||||
return normalizeArray(unwrapWasmResponse(responseData)).map(parseWasmData);
|
||||
}
|
||||
|
||||
// Returns the data at the key if present (unknown decoded json),
|
||||
// or null if no data at this (contract address, key) pair
|
||||
public async queryContractRaw(address: string, key: Uint8Array): Promise<Uint8Array | null> {
|
||||
const hexKey = toHex(key);
|
||||
const path = `/wasm/contract/${address}/raw/${hexKey}?encoding=hex`;
|
||||
const responseData = (await this.get(path)) as WasmResponse<WasmData[]>;
|
||||
const data = unwrapWasmResponse(responseData);
|
||||
return data.length === 0 ? null : fromBase64(data[0].val);
|
||||
}
|
||||
|
||||
/**
|
||||
* Makes a smart query on the contract and parses the reponse as JSON.
|
||||
* Throws error if no such contract exists, the query format is invalid or the response is invalid.
|
||||
*/
|
||||
public async queryContractSmart(address: string, query: object): Promise<JsonObject> {
|
||||
const encoded = toHex(toUtf8(JSON.stringify(query)));
|
||||
const path = `/wasm/contract/${address}/smart/${encoded}?encoding=hex`;
|
||||
const responseData = (await this.get(path)) as WasmResponse<SmartQueryResponse>;
|
||||
const result = unwrapWasmResponse(responseData);
|
||||
// By convention, smart queries must return a valid JSON document (see https://github.com/CosmWasm/cosmwasm/issues/144)
|
||||
return JSON.parse(fromUtf8(fromBase64(result.smart)));
|
||||
}
|
||||
}
|
||||
30
packages/sdk38/src/sequence.spec.ts
Normal file
30
packages/sdk38/src/sequence.spec.ts
Normal file
@ -0,0 +1,30 @@
|
||||
import { findSequenceForSignedTx } from "./sequence";
|
||||
import response1 from "./testdata/txresponse1.json";
|
||||
import response2 from "./testdata/txresponse2.json";
|
||||
import response3 from "./testdata/txresponse3.json";
|
||||
|
||||
// Those values must match ./testdata/txresponse*.json
|
||||
const chainId = "testing";
|
||||
const accountNumber = 4;
|
||||
|
||||
describe("sequence", () => {
|
||||
describe("findSequenceForSignedTx", () => {
|
||||
it("works", async () => {
|
||||
const current = 100; // what we get from GET /auth/accounts/{address}
|
||||
expect(await findSequenceForSignedTx(response1.tx, chainId, accountNumber, current)).toEqual(10);
|
||||
// We know response3.height > response1.height, so the sequence must be at least 10+1
|
||||
expect(await findSequenceForSignedTx(response3.tx, chainId, accountNumber, current, 11)).toEqual(19);
|
||||
// We know response3.height > response2.height > response1.height, so the sequence must be at least 10+1 and smaller than 19
|
||||
expect(await findSequenceForSignedTx(response2.tx, chainId, accountNumber, 19, 11)).toEqual(13);
|
||||
});
|
||||
|
||||
it("returns undefined when sequence is not in range", async () => {
|
||||
expect(await findSequenceForSignedTx(response1.tx, chainId, accountNumber, 5)).toBeUndefined();
|
||||
expect(await findSequenceForSignedTx(response1.tx, chainId, accountNumber, 20, 11)).toBeUndefined();
|
||||
expect(await findSequenceForSignedTx(response1.tx, chainId, accountNumber, 20, 50)).toBeUndefined();
|
||||
|
||||
// upper bound is not included in the possible results
|
||||
expect(await findSequenceForSignedTx(response1.tx, chainId, accountNumber, 10)).toBeUndefined();
|
||||
});
|
||||
});
|
||||
});
|
||||
46
packages/sdk38/src/sequence.ts
Normal file
46
packages/sdk38/src/sequence.ts
Normal file
@ -0,0 +1,46 @@
|
||||
import { Secp256k1, Secp256k1Signature, Sha256 } from "@iov/crypto";
|
||||
|
||||
import { makeSignBytes } from "./encoding";
|
||||
import { decodeSignature } from "./signature";
|
||||
import { CosmosSdkTx } from "./types";
|
||||
|
||||
/**
|
||||
* Serach for sequence s with `min` <= `s` < `upperBound` to find the sequence that was used to sign the transaction
|
||||
*
|
||||
* @param tx The signed transaction
|
||||
* @param chainId The chain ID for which this transaction was signed
|
||||
* @param accountNumber The account number for which this transaction was signed
|
||||
* @param upperBound The upper bound for the testing, i.e. sequence must be lower than this value
|
||||
* @param min The lowest sequence that is tested
|
||||
*
|
||||
* @returns the sequence if a match was found and undefined otherwise
|
||||
*/
|
||||
export async function findSequenceForSignedTx(
|
||||
tx: CosmosSdkTx,
|
||||
chainId: string,
|
||||
accountNumber: number,
|
||||
upperBound: number,
|
||||
min = 0,
|
||||
): Promise<number | undefined> {
|
||||
const firstSignature = tx.value.signatures.find(() => true);
|
||||
if (!firstSignature) throw new Error("Signature missing in tx");
|
||||
|
||||
const { pubkey, signature } = decodeSignature(firstSignature);
|
||||
const secp256keSignature = Secp256k1Signature.fromFixedLength(signature);
|
||||
|
||||
for (let s = min; s < upperBound; s++) {
|
||||
// console.log(`Trying sequence ${s}`);
|
||||
const signBytes = makeSignBytes(
|
||||
tx.value.msg,
|
||||
tx.value.fee,
|
||||
chainId,
|
||||
tx.value.memo || "",
|
||||
accountNumber,
|
||||
s,
|
||||
);
|
||||
const prehashed = new Sha256(signBytes).digest();
|
||||
const valid = await Secp256k1.verifySignature(secp256keSignature, prehashed, pubkey);
|
||||
if (valid) return s;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
69
packages/sdk38/src/signature.spec.ts
Normal file
69
packages/sdk38/src/signature.spec.ts
Normal file
@ -0,0 +1,69 @@
|
||||
import { Encoding } from "@iov/encoding";
|
||||
|
||||
import { decodeSignature, encodeSecp256k1Signature } from "./signature";
|
||||
import { StdSignature } from "./types";
|
||||
|
||||
const { fromBase64 } = Encoding;
|
||||
|
||||
describe("signature", () => {
|
||||
describe("encodeSecp256k1Signature", () => {
|
||||
it("encodes a full signature", () => {
|
||||
const pubkey = fromBase64("AtQaCqFnshaZQp6rIkvAPyzThvCvXSDO+9AzbxVErqJP");
|
||||
const signature = fromBase64(
|
||||
"1nUcIH0CLT0/nQ0mBTDrT6kMG20NY/PsH7P2gc4bpYNGLEYjBmdWevXUJouSE/9A/60QG9cYeqyTe5kFDeIPxQ==",
|
||||
);
|
||||
expect(encodeSecp256k1Signature(pubkey, signature)).toEqual({
|
||||
// eslint-disable-next-line @typescript-eslint/camelcase
|
||||
pub_key: {
|
||||
type: "tendermint/PubKeySecp256k1",
|
||||
value: "AtQaCqFnshaZQp6rIkvAPyzThvCvXSDO+9AzbxVErqJP",
|
||||
},
|
||||
signature: "1nUcIH0CLT0/nQ0mBTDrT6kMG20NY/PsH7P2gc4bpYNGLEYjBmdWevXUJouSE/9A/60QG9cYeqyTe5kFDeIPxQ==",
|
||||
});
|
||||
});
|
||||
|
||||
it("throws when getting uncompressed public keys", () => {
|
||||
const pubkey = fromBase64(
|
||||
"BE8EGB7ro1ORuFhjOnZcSgwYlpe0DSFjVNUIkNNQxwKQE7WHpoHoNswYeoFkuYpYSKK4mzFzMV/dB0DVAy4lnNU=",
|
||||
);
|
||||
const signature = fromBase64(
|
||||
"1nUcIH0CLT0/nQ0mBTDrT6kMG20NY/PsH7P2gc4bpYNGLEYjBmdWevXUJouSE/9A/60QG9cYeqyTe5kFDeIPxQ==",
|
||||
);
|
||||
expect(() => encodeSecp256k1Signature(pubkey, signature)).toThrowError(
|
||||
/public key must be compressed secp256k1/i,
|
||||
);
|
||||
});
|
||||
|
||||
it("throws if signature contains recovery byte", () => {
|
||||
const pubkey = fromBase64("AtQaCqFnshaZQp6rIkvAPyzThvCvXSDO+9AzbxVErqJP");
|
||||
const signature = Uint8Array.from([
|
||||
...fromBase64(
|
||||
"1nUcIH0CLT0/nQ0mBTDrT6kMG20NY/PsH7P2gc4bpYNGLEYjBmdWevXUJouSE/9A/60QG9cYeqyTe5kFDeIPxQ==",
|
||||
),
|
||||
99,
|
||||
]);
|
||||
expect(() => encodeSecp256k1Signature(pubkey, signature)).toThrowError(
|
||||
/signature must be 64 bytes long/i,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("decodeSignature", () => {
|
||||
it("works for secp256k1", () => {
|
||||
const signature: StdSignature = {
|
||||
// eslint-disable-next-line @typescript-eslint/camelcase
|
||||
pub_key: {
|
||||
type: "tendermint/PubKeySecp256k1",
|
||||
value: "AtQaCqFnshaZQp6rIkvAPyzThvCvXSDO+9AzbxVErqJP",
|
||||
},
|
||||
signature: "1nUcIH0CLT0/nQ0mBTDrT6kMG20NY/PsH7P2gc4bpYNGLEYjBmdWevXUJouSE/9A/60QG9cYeqyTe5kFDeIPxQ==",
|
||||
};
|
||||
expect(decodeSignature(signature)).toEqual({
|
||||
pubkey: fromBase64("AtQaCqFnshaZQp6rIkvAPyzThvCvXSDO+9AzbxVErqJP"),
|
||||
signature: fromBase64(
|
||||
"1nUcIH0CLT0/nQ0mBTDrT6kMG20NY/PsH7P2gc4bpYNGLEYjBmdWevXUJouSE/9A/60QG9cYeqyTe5kFDeIPxQ==",
|
||||
),
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
39
packages/sdk38/src/signature.ts
Normal file
39
packages/sdk38/src/signature.ts
Normal file
@ -0,0 +1,39 @@
|
||||
import { Encoding } from "@iov/encoding";
|
||||
|
||||
import { encodeSecp256k1Pubkey } from "./pubkey";
|
||||
import { pubkeyType, StdSignature } from "./types";
|
||||
|
||||
/**
|
||||
* Takes a binary pubkey and signature to create a signature object
|
||||
*
|
||||
* @param pubkey a compressed secp256k1 public key
|
||||
* @param signature a 64 byte fixed length representation of secp256k1 signature components r and s
|
||||
*/
|
||||
export function encodeSecp256k1Signature(pubkey: Uint8Array, signature: Uint8Array): StdSignature {
|
||||
if (signature.length !== 64) {
|
||||
throw new Error(
|
||||
"Signature must be 64 bytes long. Cosmos SDK uses a 2x32 byte fixed length encoding for the secp256k1 signature integers r and s.",
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
// eslint-disable-next-line @typescript-eslint/camelcase
|
||||
pub_key: encodeSecp256k1Pubkey(pubkey),
|
||||
signature: Encoding.toBase64(signature),
|
||||
};
|
||||
}
|
||||
|
||||
export function decodeSignature(
|
||||
signature: StdSignature,
|
||||
): { readonly pubkey: Uint8Array; readonly signature: Uint8Array } {
|
||||
switch (signature.pub_key.type) {
|
||||
// Note: please don't add cases here without writing additional unit tests
|
||||
case pubkeyType.secp256k1:
|
||||
return {
|
||||
pubkey: Encoding.fromBase64(signature.pub_key.value),
|
||||
signature: Encoding.fromBase64(signature.signature),
|
||||
};
|
||||
default:
|
||||
throw new Error("Unsupported pubkey type");
|
||||
}
|
||||
}
|
||||
78
packages/sdk38/src/signingcosmosclient.spec.ts
Normal file
78
packages/sdk38/src/signingcosmosclient.spec.ts
Normal file
@ -0,0 +1,78 @@
|
||||
import { assert } from "@iov/utils";
|
||||
|
||||
import { Coin } from "./coins";
|
||||
import { PrivateCosmWasmClient } from "./cosmosclient";
|
||||
import { Secp256k1Pen } from "./pen";
|
||||
import { SigningCosmosClient } from "./signingcosmosclient";
|
||||
import { makeRandomAddress, pendingWithoutWasmd } from "./testutils.spec";
|
||||
|
||||
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("SigningCosmosClient", () => {
|
||||
describe("makeReadOnly", () => {
|
||||
it("can be constructed", async () => {
|
||||
const pen = await Secp256k1Pen.fromMnemonic(faucet.mnemonic);
|
||||
const client = new SigningCosmosClient(httpUrl, faucet.address, (signBytes) => pen.sign(signBytes));
|
||||
expect(client).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
describe("getHeight", () => {
|
||||
it("always uses authAccount implementation", async () => {
|
||||
pendingWithoutWasmd();
|
||||
const pen = await Secp256k1Pen.fromMnemonic(faucet.mnemonic);
|
||||
const client = new SigningCosmosClient(httpUrl, faucet.address, (signBytes) => pen.sign(signBytes));
|
||||
|
||||
const openedClient = (client as unknown) as PrivateCosmWasmClient;
|
||||
const blockLatestSpy = spyOn(openedClient.restClient, "blocksLatest").and.callThrough();
|
||||
const authAccountsSpy = spyOn(openedClient.restClient, "authAccounts").and.callThrough();
|
||||
|
||||
const height = await client.getHeight();
|
||||
expect(height).toBeGreaterThan(0);
|
||||
|
||||
expect(blockLatestSpy).toHaveBeenCalledTimes(0);
|
||||
expect(authAccountsSpy).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe("sendTokens", () => {
|
||||
it("works", async () => {
|
||||
pendingWithoutWasmd();
|
||||
const pen = await Secp256k1Pen.fromMnemonic(faucet.mnemonic);
|
||||
const client = new SigningCosmosClient(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.balance).toEqual(transferAmount);
|
||||
});
|
||||
});
|
||||
});
|
||||
145
packages/sdk38/src/signingcosmosclient.ts
Normal file
145
packages/sdk38/src/signingcosmosclient.ts
Normal file
@ -0,0 +1,145 @@
|
||||
import { Coin, coins } from "./coins";
|
||||
import { Account, CosmosClient, GetNonceResult, PostTxResult } from "./cosmosclient";
|
||||
import { makeSignBytes } from "./encoding";
|
||||
import { Log } from "./logs";
|
||||
import { BroadcastMode } from "./restclient";
|
||||
import { MsgSend, 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;
|
||||
}
|
||||
|
||||
const defaultFees: FeeTable = {
|
||||
upload: {
|
||||
amount: coins(25000, "ucosm"),
|
||||
gas: "1000000", // one million
|
||||
},
|
||||
init: {
|
||||
amount: coins(12500, "ucosm"),
|
||||
gas: "500000", // 500k
|
||||
},
|
||||
exec: {
|
||||
amount: coins(5000, "ucosm"),
|
||||
gas: "200000", // 200k
|
||||
},
|
||||
send: {
|
||||
amount: coins(2000, "ucosm"),
|
||||
gas: "80000", // 80k
|
||||
},
|
||||
};
|
||||
|
||||
export interface UploadMeta {
|
||||
/** The source URL */
|
||||
readonly source?: string;
|
||||
/** The builder tag */
|
||||
readonly builder?: string;
|
||||
}
|
||||
|
||||
export interface UploadResult {
|
||||
/** Size of the original wasm code in bytes */
|
||||
readonly originalSize: number;
|
||||
/** A hex encoded sha256 checksum of the original wasm code (that is stored on chain) */
|
||||
readonly originalChecksum: string;
|
||||
/** Size of the compressed wasm code in bytes */
|
||||
readonly compressedSize: number;
|
||||
/** A hex encoded sha256 checksum of the compressed wasm code (that stored in the transaction) */
|
||||
readonly compressedChecksum: string;
|
||||
/** The ID of the code asigned by the chain */
|
||||
readonly codeId: number;
|
||||
readonly logs: readonly Log[];
|
||||
/** Transaction hash (might be used as transaction ID). Guaranteed to be non-empty upper-case hex */
|
||||
readonly transactionHash: string;
|
||||
}
|
||||
|
||||
export interface InstantiateResult {
|
||||
/** The address of the newly instantiated contract */
|
||||
readonly contractAddress: string;
|
||||
readonly logs: readonly Log[];
|
||||
/** Transaction hash (might be used as transaction ID). Guaranteed to be non-empty upper-case hex */
|
||||
readonly transactionHash: string;
|
||||
}
|
||||
|
||||
export interface ExecuteResult {
|
||||
readonly logs: readonly Log[];
|
||||
/** Transaction hash (might be used as transaction ID). Guaranteed to be non-empty upper-case hex */
|
||||
readonly transactionHash: string;
|
||||
}
|
||||
|
||||
export class SigningCosmosClient extends CosmosClient {
|
||||
public readonly senderAddress: string;
|
||||
|
||||
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(
|
||||
apiUrl: string,
|
||||
senderAddress: string,
|
||||
signCallback: SigningCallback,
|
||||
customFees?: Partial<FeeTable>,
|
||||
broadcastMode = BroadcastMode.Block,
|
||||
) {
|
||||
super(apiUrl, broadcastMode);
|
||||
this.anyValidAddress = senderAddress;
|
||||
|
||||
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<Account | undefined> {
|
||||
return super.getAccount(address || this.senderAddress);
|
||||
}
|
||||
|
||||
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.getChainId();
|
||||
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(signedTx);
|
||||
}
|
||||
}
|
||||
44
packages/sdk38/src/testdata/cosmoshub.json
vendored
Normal file
44
packages/sdk38/src/testdata/cosmoshub.json
vendored
Normal file
@ -0,0 +1,44 @@
|
||||
{
|
||||
"//source": "https://hubble.figment.network/cosmos/chains/cosmoshub-3/blocks/415777/transactions/2BD600EA6090FC75FD844CA73542CC90A828770F4C01C5B483C3C1C43CCB65F4?format=json",
|
||||
"tx": {
|
||||
"type": "cosmos-sdk/StdTx",
|
||||
"value": {
|
||||
"msg": [
|
||||
{
|
||||
"type": "cosmos-sdk/MsgSend",
|
||||
"value": {
|
||||
"from_address": "cosmos1txqfn5jmcts0x0q7krdxj8tgf98tj0965vqlmq",
|
||||
"to_address": "cosmos1nynns8ex9fq6sjjfj8k79ymkdz4sqth06xexae",
|
||||
"amount": [
|
||||
{
|
||||
"denom": "uatom",
|
||||
"amount": "35997500"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
],
|
||||
"fee": {
|
||||
"amount": [
|
||||
{
|
||||
"denom": "uatom",
|
||||
"amount": "2500"
|
||||
}
|
||||
],
|
||||
"gas": "100000"
|
||||
},
|
||||
"signatures": [
|
||||
{
|
||||
"pub_key": {
|
||||
"type": "tendermint/PubKeySecp256k1",
|
||||
"value": "A5qFcJBJvEK/fOmEAY0DHNWwSRZ9TEfNZyH8VoVvDtAq"
|
||||
},
|
||||
"signature": "NK1Oy4EUGAsoC03c1wi9GG03JC/39LEdautC5Jk643oIbEPqeXHMwaqbdvO/Jws0X/NAXaN8SAy2KNY5Qml+5Q=="
|
||||
}
|
||||
],
|
||||
"memo": ""
|
||||
}
|
||||
},
|
||||
"tx_data": "ygEoKBapCkOoo2GaChRZgJnSW8Lg8zwesNppHWhJTrk8uhIUmSc4HyYqQahKSZHt4pN2aKsALu8aEQoFdWF0b20SCDM1OTk3NTAwEhMKDQoFdWF0b20SBDI1MDAQoI0GGmoKJuta6YchA5qFcJBJvEK/fOmEAY0DHNWwSRZ9TEfNZyH8VoVvDtAqEkA0rU7LgRQYCygLTdzXCL0YbTckL/f0sR1q60LkmTrjeghsQ+p5cczBqpt2878nCzRf80Bdo3xIDLYo1jlCaX7l",
|
||||
"id": "2BD600EA6090FC75FD844CA73542CC90A828770F4C01C5B483C3C1C43CCB65F4"
|
||||
}
|
||||
57
packages/sdk38/src/testdata/txresponse1.json
vendored
Normal file
57
packages/sdk38/src/testdata/txresponse1.json
vendored
Normal file
@ -0,0 +1,57 @@
|
||||
{
|
||||
"height": "15888",
|
||||
"txhash": "672DEDE8EF4DE8B5818959F417CCA357079D4D7A19C4B65443C7FBF8176AABF9",
|
||||
"raw_log": "[{\"msg_index\":0,\"log\":\"\",\"events\":[{\"type\":\"message\",\"attributes\":[{\"key\":\"action\",\"value\":\"send\"},{\"key\":\"sender\",\"value\":\"cosmos1pkptre7fdkl6gfrzlesjjvhxhlc3r4gmmk8rs6\"},{\"key\":\"module\",\"value\":\"bank\"}]},{\"type\":\"transfer\",\"attributes\":[{\"key\":\"recipient\",\"value\":\"cosmos1t70qnpr0az8tf7py83m4ue5y89w58lkjmx0yq2\"},{\"key\":\"amount\",\"value\":\"75000ucosm\"}]}]}]",
|
||||
"logs": [
|
||||
{
|
||||
"msg_index": 0,
|
||||
"log": "",
|
||||
"events": [
|
||||
{
|
||||
"type": "message",
|
||||
"attributes": [
|
||||
{ "key": "action", "value": "send" },
|
||||
{ "key": "sender", "value": "cosmos1pkptre7fdkl6gfrzlesjjvhxhlc3r4gmmk8rs6" },
|
||||
{ "key": "module", "value": "bank" }
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "transfer",
|
||||
"attributes": [
|
||||
{ "key": "recipient", "value": "cosmos1t70qnpr0az8tf7py83m4ue5y89w58lkjmx0yq2" },
|
||||
{ "key": "amount", "value": "75000ucosm" }
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"gas_wanted": "200000",
|
||||
"gas_used": "65407",
|
||||
"tx": {
|
||||
"type": "cosmos-sdk/StdTx",
|
||||
"value": {
|
||||
"msg": [
|
||||
{
|
||||
"type": "cosmos-sdk/MsgSend",
|
||||
"value": {
|
||||
"from_address": "cosmos1pkptre7fdkl6gfrzlesjjvhxhlc3r4gmmk8rs6",
|
||||
"to_address": "cosmos1t70qnpr0az8tf7py83m4ue5y89w58lkjmx0yq2",
|
||||
"amount": [{ "denom": "ucosm", "amount": "75000" }]
|
||||
}
|
||||
}
|
||||
],
|
||||
"fee": { "amount": [{ "denom": "ucosm", "amount": "5000" }], "gas": "200000" },
|
||||
"signatures": [
|
||||
{
|
||||
"pub_key": {
|
||||
"type": "tendermint/PubKeySecp256k1",
|
||||
"value": "A08EGB7ro1ORuFhjOnZcSgwYlpe0DSFjVNUIkNNQxwKQ"
|
||||
},
|
||||
"signature": "US7oH8S/8TxVrtBQkOhHxAM+oDB2spNAEawgh6H8CCFLRMOJK+uvQZZ6ceUgUsvDbxwCz7re1RU272fymMYRZQ=="
|
||||
}
|
||||
],
|
||||
"memo": "My first payment"
|
||||
}
|
||||
},
|
||||
"timestamp": "2020-02-14T11:25:55Z"
|
||||
}
|
||||
57
packages/sdk38/src/testdata/txresponse2.json
vendored
Normal file
57
packages/sdk38/src/testdata/txresponse2.json
vendored
Normal file
@ -0,0 +1,57 @@
|
||||
{
|
||||
"height": "16456",
|
||||
"txhash": "7BFE4B93AF190F60132C62D08FDF50BE462FBCE374EB13D3FD0C32461E771EC0",
|
||||
"raw_log": "[{\"msg_index\":0,\"log\":\"\",\"events\":[{\"type\":\"message\",\"attributes\":[{\"key\":\"action\",\"value\":\"send\"},{\"key\":\"sender\",\"value\":\"cosmos1pkptre7fdkl6gfrzlesjjvhxhlc3r4gmmk8rs6\"},{\"key\":\"module\",\"value\":\"bank\"}]},{\"type\":\"transfer\",\"attributes\":[{\"key\":\"recipient\",\"value\":\"cosmos1t70qnpr0az8tf7py83m4ue5y89w58lkjmx0yq2\"},{\"key\":\"amount\",\"value\":\"75000ucosm\"}]}]}]",
|
||||
"logs": [
|
||||
{
|
||||
"msg_index": 0,
|
||||
"log": "",
|
||||
"events": [
|
||||
{
|
||||
"type": "message",
|
||||
"attributes": [
|
||||
{ "key": "action", "value": "send" },
|
||||
{ "key": "sender", "value": "cosmos1pkptre7fdkl6gfrzlesjjvhxhlc3r4gmmk8rs6" },
|
||||
{ "key": "module", "value": "bank" }
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "transfer",
|
||||
"attributes": [
|
||||
{ "key": "recipient", "value": "cosmos1t70qnpr0az8tf7py83m4ue5y89w58lkjmx0yq2" },
|
||||
{ "key": "amount", "value": "75000ucosm" }
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"gas_wanted": "200000",
|
||||
"gas_used": "65407",
|
||||
"tx": {
|
||||
"type": "cosmos-sdk/StdTx",
|
||||
"value": {
|
||||
"msg": [
|
||||
{
|
||||
"type": "cosmos-sdk/MsgSend",
|
||||
"value": {
|
||||
"from_address": "cosmos1pkptre7fdkl6gfrzlesjjvhxhlc3r4gmmk8rs6",
|
||||
"to_address": "cosmos1t70qnpr0az8tf7py83m4ue5y89w58lkjmx0yq2",
|
||||
"amount": [{ "denom": "ucosm", "amount": "75000" }]
|
||||
}
|
||||
}
|
||||
],
|
||||
"fee": { "amount": [{ "denom": "ucosm", "amount": "5000" }], "gas": "200000" },
|
||||
"signatures": [
|
||||
{
|
||||
"pub_key": {
|
||||
"type": "tendermint/PubKeySecp256k1",
|
||||
"value": "A08EGB7ro1ORuFhjOnZcSgwYlpe0DSFjVNUIkNNQxwKQ"
|
||||
},
|
||||
"signature": "ltvd9Rb3RF4zjbUVrpDpkok34g+py7XR8ZcM0tZUYRxxVdcMEin010x+ZFd/mOuutPj9fDmSENnienc/yi4msw=="
|
||||
}
|
||||
],
|
||||
"memo": "My first payment"
|
||||
}
|
||||
},
|
||||
"timestamp": "2020-02-14T11:35:41Z"
|
||||
}
|
||||
57
packages/sdk38/src/testdata/txresponse3.json
vendored
Normal file
57
packages/sdk38/src/testdata/txresponse3.json
vendored
Normal file
@ -0,0 +1,57 @@
|
||||
{
|
||||
"height": "20730",
|
||||
"txhash": "625BC75E697F73DA037387C34002BB2F682E7ACDCC4E015D3E90420516C6D0C8",
|
||||
"raw_log": "[{\"msg_index\":0,\"log\":\"\",\"events\":[{\"type\":\"message\",\"attributes\":[{\"key\":\"action\",\"value\":\"send\"},{\"key\":\"sender\",\"value\":\"cosmos1pkptre7fdkl6gfrzlesjjvhxhlc3r4gmmk8rs6\"},{\"key\":\"module\",\"value\":\"bank\"}]},{\"type\":\"transfer\",\"attributes\":[{\"key\":\"recipient\",\"value\":\"cosmos1t70qnpr0az8tf7py83m4ue5y89w58lkjmx0yq2\"},{\"key\":\"amount\",\"value\":\"75000ucosm\"}]}]}]",
|
||||
"logs": [
|
||||
{
|
||||
"msg_index": 0,
|
||||
"log": "",
|
||||
"events": [
|
||||
{
|
||||
"type": "message",
|
||||
"attributes": [
|
||||
{ "key": "action", "value": "send" },
|
||||
{ "key": "sender", "value": "cosmos1pkptre7fdkl6gfrzlesjjvhxhlc3r4gmmk8rs6" },
|
||||
{ "key": "module", "value": "bank" }
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "transfer",
|
||||
"attributes": [
|
||||
{ "key": "recipient", "value": "cosmos1t70qnpr0az8tf7py83m4ue5y89w58lkjmx0yq2" },
|
||||
{ "key": "amount", "value": "75000ucosm" }
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"gas_wanted": "200000",
|
||||
"gas_used": "65407",
|
||||
"tx": {
|
||||
"type": "cosmos-sdk/StdTx",
|
||||
"value": {
|
||||
"msg": [
|
||||
{
|
||||
"type": "cosmos-sdk/MsgSend",
|
||||
"value": {
|
||||
"from_address": "cosmos1pkptre7fdkl6gfrzlesjjvhxhlc3r4gmmk8rs6",
|
||||
"to_address": "cosmos1t70qnpr0az8tf7py83m4ue5y89w58lkjmx0yq2",
|
||||
"amount": [{ "denom": "ucosm", "amount": "75000" }]
|
||||
}
|
||||
}
|
||||
],
|
||||
"fee": { "amount": [{ "denom": "ucosm", "amount": "5000" }], "gas": "200000" },
|
||||
"signatures": [
|
||||
{
|
||||
"pub_key": {
|
||||
"type": "tendermint/PubKeySecp256k1",
|
||||
"value": "A08EGB7ro1ORuFhjOnZcSgwYlpe0DSFjVNUIkNNQxwKQ"
|
||||
},
|
||||
"signature": "eOFGl1tIHDMv3JdCK9fRSikVbYUD8+B0ksb3dJFya8MPYgpEpdSA7zZc+5n/cW6LR/BJdib4nqmJQv1yD9lm3g=="
|
||||
}
|
||||
],
|
||||
"memo": "My first payment"
|
||||
}
|
||||
},
|
||||
"timestamp": "2020-02-14T12:48:56Z"
|
||||
}
|
||||
58
packages/sdk38/src/testutils.spec.ts
Normal file
58
packages/sdk38/src/testutils.spec.ts
Normal file
@ -0,0 +1,58 @@
|
||||
import { Random } from "@iov/crypto";
|
||||
import { Bech32 } from "@iov/encoding";
|
||||
|
||||
export function makeRandomAddress(): string {
|
||||
return Bech32.encode("cosmos", Random.getBytes(20));
|
||||
}
|
||||
|
||||
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}$/;
|
||||
export const tendermintShortHashMatcher = /^[0-9a-f]{40}$/;
|
||||
export const semverMatcher = /^[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z-]+(\.[0-9A-Za-z-]+)*)?$/;
|
||||
|
||||
// https://github.com/bitcoin/bips/blob/master/bip-0173.mediawiki#bech32
|
||||
export const bech32AddressMatcher = /^[\x21-\x7e]{1,83}1[02-9ac-hj-np-z]{38}$/;
|
||||
|
||||
export const wasmd = {
|
||||
endpoint: "http://localhost:1317",
|
||||
chainId: "testing",
|
||||
};
|
||||
|
||||
export 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",
|
||||
};
|
||||
|
||||
/** Unused account */
|
||||
export const unused = {
|
||||
pubkey: {
|
||||
type: "tendermint/PubKeySecp256k1",
|
||||
value: "ArkCaFUJ/IH+vKBmNRCdUVl3mCAhbopk9jjW4Ko4OfRQ",
|
||||
},
|
||||
address: "cosmos1cjsxept9rkggzxztslae9ndgpdyt2408lk850u",
|
||||
accountNumber: 9,
|
||||
sequence: 0,
|
||||
};
|
||||
|
||||
export function wasmdEnabled(): boolean {
|
||||
return !!process.env.WASMD_ENABLED;
|
||||
}
|
||||
|
||||
export function pendingWithoutWasmd(): void {
|
||||
if (!wasmdEnabled()) {
|
||||
return pending("Set WASMD_ENABLED to enable Wasmd based tests");
|
||||
}
|
||||
}
|
||||
|
||||
/** Returns first element. Throws if array has a different length than 1. */
|
||||
export function fromOneElementArray<T>(elements: ArrayLike<T>): T {
|
||||
if (elements.length !== 1) throw new Error(`Expected exactly one element but got ${elements.length}`);
|
||||
return elements[0];
|
||||
}
|
||||
105
packages/sdk38/src/types.ts
Normal file
105
packages/sdk38/src/types.ts
Normal file
@ -0,0 +1,105 @@
|
||||
import { Encoding } from "@iov/encoding";
|
||||
|
||||
import { Coin } from "./coins";
|
||||
|
||||
const { fromBase64, fromHex } = Encoding;
|
||||
|
||||
/** An Amino/Cosmos SDK StdTx */
|
||||
export interface StdTx {
|
||||
readonly msg: ReadonlyArray<Msg>;
|
||||
readonly fee: StdFee;
|
||||
readonly signatures: ReadonlyArray<StdSignature>;
|
||||
readonly memo: string | undefined;
|
||||
}
|
||||
|
||||
export function isStdTx(txValue: unknown): txValue is StdTx {
|
||||
const { memo, msg, fee, signatures } = txValue as StdTx;
|
||||
return (
|
||||
typeof memo === "string" && Array.isArray(msg) && typeof fee === "object" && Array.isArray(signatures)
|
||||
);
|
||||
}
|
||||
|
||||
export interface CosmosSdkTx {
|
||||
readonly type: string;
|
||||
readonly value: StdTx;
|
||||
}
|
||||
|
||||
interface MsgTemplate {
|
||||
readonly type: string;
|
||||
readonly value: any;
|
||||
}
|
||||
|
||||
/** A Cosmos SDK token transfer message */
|
||||
export interface MsgSend extends MsgTemplate {
|
||||
readonly type: "cosmos-sdk/MsgSend";
|
||||
readonly value: {
|
||||
/** Bech32 account address */
|
||||
readonly from_address: string;
|
||||
/** Bech32 account address */
|
||||
readonly to_address: string;
|
||||
readonly amount: ReadonlyArray<Coin>;
|
||||
};
|
||||
}
|
||||
|
||||
export type Msg = MsgSend | MsgTemplate;
|
||||
|
||||
export function isMsgSend(msg: Msg): msg is MsgSend {
|
||||
return (msg as MsgSend).type === "cosmos-sdk/MsgSend";
|
||||
}
|
||||
|
||||
export interface StdFee {
|
||||
readonly amount: ReadonlyArray<Coin>;
|
||||
readonly gas: string;
|
||||
}
|
||||
|
||||
export interface StdSignature {
|
||||
readonly pub_key: PubKey;
|
||||
readonly signature: string;
|
||||
}
|
||||
|
||||
export interface PubKey {
|
||||
// type is one of the strings defined in pubkeyTypes
|
||||
// I don't use a string literal union here as that makes trouble with json test data:
|
||||
// https://github.com/confio/cosmwasm-js/pull/44#pullrequestreview-353280504
|
||||
readonly type: string;
|
||||
// Value field is base64-encoded in all cases
|
||||
// Note: if type is Secp256k1, this must contain a COMPRESSED pubkey - to encode from bcp/keycontrol land, you must compress it first
|
||||
readonly value: string;
|
||||
}
|
||||
|
||||
export const pubkeyType = {
|
||||
/** @see https://github.com/tendermint/tendermint/blob/v0.33.0/crypto/ed25519/ed25519.go#L22 */
|
||||
secp256k1: "tendermint/PubKeySecp256k1" as const,
|
||||
/** @see https://github.com/tendermint/tendermint/blob/v0.33.0/crypto/secp256k1/secp256k1.go#L23 */
|
||||
ed25519: "tendermint/PubKeyEd25519" as const,
|
||||
/** @see https://github.com/tendermint/tendermint/blob/v0.33.0/crypto/sr25519/codec.go#L12 */
|
||||
sr25519: "tendermint/PubKeySr25519" as const,
|
||||
};
|
||||
|
||||
export const pubkeyTypes: readonly string[] = [pubkeyType.secp256k1, pubkeyType.ed25519, pubkeyType.sr25519];
|
||||
|
||||
export interface WasmData {
|
||||
// key is hex-encoded
|
||||
readonly key: string;
|
||||
// value is base64 encoded
|
||||
readonly val: string;
|
||||
}
|
||||
|
||||
// Model is a parsed WasmData object
|
||||
export interface Model {
|
||||
readonly key: Uint8Array;
|
||||
readonly val: Uint8Array;
|
||||
}
|
||||
|
||||
export function parseWasmData({ key, val }: WasmData): Model {
|
||||
return {
|
||||
key: fromHex(key),
|
||||
val: fromBase64(val),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* An object containing a parsed JSON document. The result of JSON.parse().
|
||||
* This doen't privide any type safety over `any` but expresses intent in the code.
|
||||
*/
|
||||
export type JsonObject = any;
|
||||
12
packages/sdk38/tsconfig.json
Normal file
12
packages/sdk38/tsconfig.json
Normal file
@ -0,0 +1,12 @@
|
||||
{
|
||||
"extends": "../../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"baseUrl": ".",
|
||||
"outDir": "build",
|
||||
"declarationDir": "build/types",
|
||||
"rootDir": "src"
|
||||
},
|
||||
"include": [
|
||||
"src/**/*"
|
||||
]
|
||||
}
|
||||
14
packages/sdk38/typedoc.js
Normal file
14
packages/sdk38/typedoc.js
Normal file
@ -0,0 +1,14 @@
|
||||
const packageJson = require("./package.json");
|
||||
|
||||
module.exports = {
|
||||
src: ["./src"],
|
||||
out: "docs",
|
||||
exclude: "**/*.spec.ts",
|
||||
target: "es6",
|
||||
name: `${packageJson.name} Documentation`,
|
||||
readme: "README.md",
|
||||
mode: "file",
|
||||
excludeExternals: true,
|
||||
excludeNotExported: true,
|
||||
excludePrivate: true,
|
||||
};
|
||||
3
packages/sdk38/types/address.d.ts
vendored
Normal file
3
packages/sdk38/types/address.d.ts
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
import { PubKey } from "./types";
|
||||
export declare function rawSecp256k1PubkeyToAddress(pubkeyRaw: Uint8Array, prefix: string): string;
|
||||
export declare function pubkeyToAddress(pubkey: PubKey, prefix: string): string;
|
||||
8
packages/sdk38/types/coins.d.ts
vendored
Normal file
8
packages/sdk38/types/coins.d.ts
vendored
Normal file
@ -0,0 +1,8 @@
|
||||
export interface Coin {
|
||||
readonly denom: string;
|
||||
readonly amount: string;
|
||||
}
|
||||
/** Creates a coin */
|
||||
export declare function coin(amount: number, denom: string): Coin;
|
||||
/** Creates a list of coins with one element */
|
||||
export declare function coins(amount: number, denom: string): Coin[];
|
||||
175
packages/sdk38/types/cosmosclient.d.ts
vendored
Normal file
175
packages/sdk38/types/cosmosclient.d.ts
vendored
Normal file
@ -0,0 +1,175 @@
|
||||
import { Coin } from "./coins";
|
||||
import { Log } from "./logs";
|
||||
import { BroadcastMode, RestClient } from "./restclient";
|
||||
import { CosmosSdkTx, JsonObject, PubKey, StdTx } from "./types";
|
||||
export interface GetNonceResult {
|
||||
readonly accountNumber: number;
|
||||
readonly sequence: number;
|
||||
}
|
||||
export interface Account {
|
||||
/** Bech32 account address */
|
||||
readonly address: string;
|
||||
readonly balance: ReadonlyArray<Coin>;
|
||||
readonly pubkey: PubKey | undefined;
|
||||
readonly accountNumber: number;
|
||||
readonly sequence: number;
|
||||
}
|
||||
export interface PostTxResult {
|
||||
readonly logs: readonly Log[];
|
||||
readonly rawLog: string;
|
||||
/** Transaction hash (might be used as transaction ID). Guaranteed to be non-empty upper-case hex */
|
||||
readonly transactionHash: string;
|
||||
}
|
||||
export interface SearchByIdQuery {
|
||||
readonly id: string;
|
||||
}
|
||||
export interface SearchByHeightQuery {
|
||||
readonly height: number;
|
||||
}
|
||||
export interface SearchBySentFromOrToQuery {
|
||||
readonly sentFromOrTo: string;
|
||||
}
|
||||
/**
|
||||
* This query type allows you to pass arbitrary key/value pairs to the backend. It is
|
||||
* more powerful and slightly lower level than the other search options.
|
||||
*/
|
||||
export interface SearchByTagsQuery {
|
||||
readonly tags: readonly {
|
||||
readonly key: string;
|
||||
readonly value: string;
|
||||
}[];
|
||||
}
|
||||
export declare type SearchTxQuery =
|
||||
| SearchByIdQuery
|
||||
| SearchByHeightQuery
|
||||
| SearchBySentFromOrToQuery
|
||||
| SearchByTagsQuery;
|
||||
export interface SearchTxFilter {
|
||||
readonly minHeight?: number;
|
||||
readonly maxHeight?: number;
|
||||
}
|
||||
export interface Code {
|
||||
readonly id: number;
|
||||
/** Bech32 account address */
|
||||
readonly creator: string;
|
||||
/** Hex-encoded sha256 hash of the code stored here */
|
||||
readonly checksum: string;
|
||||
readonly source?: string;
|
||||
readonly builder?: string;
|
||||
}
|
||||
export interface CodeDetails extends Code {
|
||||
/** The original wasm bytes */
|
||||
readonly data: Uint8Array;
|
||||
}
|
||||
export interface Contract {
|
||||
readonly address: string;
|
||||
readonly codeId: number;
|
||||
/** Bech32 account address */
|
||||
readonly creator: string;
|
||||
readonly label: string;
|
||||
}
|
||||
export interface ContractDetails extends Contract {
|
||||
/** Argument passed on initialization of the contract */
|
||||
readonly initMsg: object;
|
||||
}
|
||||
/** A transaction that is indexed as part of the transaction history */
|
||||
export interface IndexedTx {
|
||||
readonly height: number;
|
||||
/** Transaction hash (might be used as transaction ID). Guaranteed to be non-empty upper-case hex */
|
||||
readonly hash: string;
|
||||
/** Transaction execution error code. 0 on success. */
|
||||
readonly code: number;
|
||||
readonly rawLog: string;
|
||||
readonly logs: readonly Log[];
|
||||
readonly tx: CosmosSdkTx;
|
||||
/** The gas limit as set by the user */
|
||||
readonly gasWanted?: number;
|
||||
/** The gas used by the execution */
|
||||
readonly gasUsed?: number;
|
||||
/** An RFC 3339 time string like e.g. '2020-02-15T10:39:10.4696305Z' */
|
||||
readonly timestamp: string;
|
||||
}
|
||||
export interface BlockHeader {
|
||||
readonly version: {
|
||||
readonly block: string;
|
||||
readonly app: string;
|
||||
};
|
||||
readonly height: number;
|
||||
readonly chainId: string;
|
||||
/** An RFC 3339 time string like e.g. '2020-02-15T10:39:10.4696305Z' */
|
||||
readonly time: string;
|
||||
}
|
||||
export interface Block {
|
||||
/** The ID is a hash of the block header (uppercase hex) */
|
||||
readonly id: string;
|
||||
readonly header: BlockHeader;
|
||||
/** Array of raw transactions */
|
||||
readonly txs: ReadonlyArray<Uint8Array>;
|
||||
}
|
||||
/** Use for testing only */
|
||||
export interface PrivateCosmWasmClient {
|
||||
readonly restClient: RestClient;
|
||||
}
|
||||
export declare class CosmosClient {
|
||||
protected readonly restClient: RestClient;
|
||||
/** Any address the chain considers valid (valid bech32 with proper prefix) */
|
||||
protected anyValidAddress: string | undefined;
|
||||
private readonly codesCache;
|
||||
private chainId;
|
||||
/**
|
||||
* 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>;
|
||||
/**
|
||||
* Returns a 32 byte upper-case hex transaction hash (typically used as the transaction ID)
|
||||
*/
|
||||
getIdentifier(tx: CosmosSdkTx): Promise<string>;
|
||||
/**
|
||||
* Returns account number and sequence.
|
||||
*
|
||||
* Throws if the account does not exist on chain.
|
||||
*
|
||||
* @param address returns data for this address. When unset, the client's sender adddress is used.
|
||||
*/
|
||||
getNonce(address: string): Promise<GetNonceResult>;
|
||||
getAccount(address: string): Promise<Account | undefined>;
|
||||
/**
|
||||
* Gets block header and meta
|
||||
*
|
||||
* @param height The height of the block. If undefined, the latest height is used.
|
||||
*/
|
||||
getBlock(height?: number): Promise<Block>;
|
||||
searchTx(query: SearchTxQuery, filter?: SearchTxFilter): Promise<readonly IndexedTx[]>;
|
||||
postTx(tx: StdTx): Promise<PostTxResult>;
|
||||
getCodes(): Promise<readonly Code[]>;
|
||||
getCodeDetails(codeId: number): Promise<CodeDetails>;
|
||||
getContracts(codeId: number): Promise<readonly Contract[]>;
|
||||
/**
|
||||
* Throws an error if no contract was found at the address
|
||||
*/
|
||||
getContract(address: string): Promise<ContractDetails>;
|
||||
/**
|
||||
* Returns the data at the key if present (raw contract dependent storage data)
|
||||
* or null if no data at this key.
|
||||
*
|
||||
* Promise is rejected when contract does not exist.
|
||||
*/
|
||||
queryContractRaw(address: string, key: Uint8Array): Promise<Uint8Array | null>;
|
||||
/**
|
||||
* Makes a smart query on the contract, returns the parsed JSON document.
|
||||
*
|
||||
* Promise is rejected when contract does not exist.
|
||||
* Promise is rejected for invalid query format.
|
||||
* Promise is rejected for invalid response format.
|
||||
*/
|
||||
queryContractSmart(address: string, queryMsg: object): Promise<JsonObject>;
|
||||
private txsQuery;
|
||||
}
|
||||
2
packages/sdk38/types/decoding.d.ts
vendored
Normal file
2
packages/sdk38/types/decoding.d.ts
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
import { StdTx } from "./types";
|
||||
export declare function unmarshalTx(data: Uint8Array): StdTx;
|
||||
10
packages/sdk38/types/encoding.d.ts
vendored
Normal file
10
packages/sdk38/types/encoding.d.ts
vendored
Normal file
@ -0,0 +1,10 @@
|
||||
import { Msg, StdFee, StdTx } from "./types";
|
||||
export declare function marshalTx(tx: StdTx): Uint8Array;
|
||||
export declare function makeSignBytes(
|
||||
msgs: readonly Msg[],
|
||||
fee: StdFee,
|
||||
chainId: string,
|
||||
memo: string,
|
||||
accountNumber: number,
|
||||
sequence: number,
|
||||
): Uint8Array;
|
||||
11
packages/sdk38/types/index.d.ts
vendored
Normal file
11
packages/sdk38/types/index.d.ts
vendored
Normal file
@ -0,0 +1,11 @@
|
||||
import * as logs from "./logs";
|
||||
import * as types from "./types";
|
||||
export { logs, types };
|
||||
export { pubkeyToAddress } from "./address";
|
||||
export { Coin, coin, coins } from "./coins";
|
||||
export { unmarshalTx } from "./decoding";
|
||||
export { makeSignBytes, marshalTx } from "./encoding";
|
||||
export { BroadcastMode, RestClient, TxsResponse } from "./restclient";
|
||||
export { decodeBech32Pubkey, encodeBech32Pubkey, encodeSecp256k1Pubkey } from "./pubkey";
|
||||
export { findSequenceForSignedTx } from "./sequence";
|
||||
export { encodeSecp256k1Signature, decodeSignature } from "./signature";
|
||||
28
packages/sdk38/types/logs.d.ts
vendored
Normal file
28
packages/sdk38/types/logs.d.ts
vendored
Normal file
@ -0,0 +1,28 @@
|
||||
export interface Attribute {
|
||||
readonly key: string;
|
||||
readonly value: string;
|
||||
}
|
||||
export interface Event {
|
||||
readonly type: string;
|
||||
readonly attributes: readonly Attribute[];
|
||||
}
|
||||
export interface Log {
|
||||
readonly msg_index: number;
|
||||
readonly log: string;
|
||||
readonly events: readonly Event[];
|
||||
}
|
||||
export declare function parseAttribute(input: unknown): Attribute;
|
||||
export declare function parseEvent(input: unknown): Event;
|
||||
export declare function parseLog(input: unknown): Log;
|
||||
export declare function parseLogs(input: unknown): readonly Log[];
|
||||
/**
|
||||
* Searches in logs for the first event of the given event type and in that event
|
||||
* for the first first attribute with the given attribute key.
|
||||
*
|
||||
* Throws if the attribute was not found.
|
||||
*/
|
||||
export declare function findAttribute(
|
||||
logs: readonly Log[],
|
||||
eventType: "message" | "transfer",
|
||||
attrKey: string,
|
||||
): Attribute;
|
||||
33
packages/sdk38/types/pen.d.ts
vendored
Normal file
33
packages/sdk38/types/pen.d.ts
vendored
Normal file
@ -0,0 +1,33 @@
|
||||
import { Slip10RawIndex } from "@iov/crypto";
|
||||
import { StdSignature } from "./types";
|
||||
export declare type PrehashType = "sha256" | "sha512" | null;
|
||||
/**
|
||||
* A pen is the most basic tool you can think of for signing. It works
|
||||
* everywhere and can be used intuitively by everyone. However, it does not
|
||||
* come with a great amount of features. End of semi suitable metaphor.
|
||||
*
|
||||
* This wraps a single keypair and allows for signing.
|
||||
*
|
||||
* Non-goals of this types are: multi account support, persistency, data migrations,
|
||||
* obfuscation of sensitive data.
|
||||
*/
|
||||
export interface Pen {
|
||||
readonly pubkey: Uint8Array;
|
||||
readonly sign: (signBytes: Uint8Array, prehashType?: PrehashType) => Promise<StdSignature>;
|
||||
}
|
||||
/**
|
||||
* The Cosmoshub derivation path in the form `m/44'/118'/0'/0/a`
|
||||
* with 0-based account index `a`.
|
||||
*/
|
||||
export declare function makeCosmoshubPath(a: number): readonly Slip10RawIndex[];
|
||||
export declare class Secp256k1Pen implements Pen {
|
||||
static fromMnemonic(mnemonic: string, hdPath?: readonly Slip10RawIndex[]): Promise<Secp256k1Pen>;
|
||||
readonly pubkey: Uint8Array;
|
||||
private readonly privkey;
|
||||
private constructor();
|
||||
/**
|
||||
* Creates and returns a signature
|
||||
*/
|
||||
sign(signBytes: Uint8Array, prehashType?: PrehashType): Promise<StdSignature>;
|
||||
address(prefix: string): string;
|
||||
}
|
||||
4
packages/sdk38/types/pubkey.d.ts
vendored
Normal file
4
packages/sdk38/types/pubkey.d.ts
vendored
Normal file
@ -0,0 +1,4 @@
|
||||
import { PubKey } from "./types";
|
||||
export declare function encodeSecp256k1Pubkey(pubkey: Uint8Array): PubKey;
|
||||
export declare function decodeBech32Pubkey(bechEncoded: string): PubKey;
|
||||
export declare function encodeBech32Pubkey(pubkey: PubKey, prefix: string): string;
|
||||
231
packages/sdk38/types/restclient.d.ts
vendored
Normal file
231
packages/sdk38/types/restclient.d.ts
vendored
Normal file
@ -0,0 +1,231 @@
|
||||
import { Coin } from "./coins";
|
||||
import { CosmosSdkTx, JsonObject, Model, StdTx } from "./types";
|
||||
export interface CosmosSdkAccount {
|
||||
/** Bech32 account address */
|
||||
readonly address: string;
|
||||
readonly coins: ReadonlyArray<Coin>;
|
||||
/** Bech32 encoded pubkey */
|
||||
readonly public_key: string;
|
||||
readonly account_number: number;
|
||||
readonly sequence: number;
|
||||
}
|
||||
export interface NodeInfo {
|
||||
readonly protocol_version: {
|
||||
readonly p2p: string;
|
||||
readonly block: string;
|
||||
readonly app: string;
|
||||
};
|
||||
readonly id: string;
|
||||
readonly listen_addr: string;
|
||||
readonly network: string;
|
||||
readonly version: string;
|
||||
readonly channels: string;
|
||||
readonly moniker: string;
|
||||
readonly other: {
|
||||
readonly tx_index: string;
|
||||
readonly rpc_address: string;
|
||||
};
|
||||
}
|
||||
export interface ApplicationVersion {
|
||||
readonly name: string;
|
||||
readonly server_name: string;
|
||||
readonly client_name: string;
|
||||
readonly version: string;
|
||||
readonly commit: string;
|
||||
readonly build_tags: string;
|
||||
readonly go: string;
|
||||
}
|
||||
export interface NodeInfoResponse {
|
||||
readonly node_info: NodeInfo;
|
||||
readonly application_version: ApplicationVersion;
|
||||
}
|
||||
export interface BlockId {
|
||||
readonly hash: string;
|
||||
}
|
||||
export interface BlockHeader {
|
||||
readonly version: {
|
||||
readonly block: string;
|
||||
readonly app: string;
|
||||
};
|
||||
readonly height: string;
|
||||
readonly chain_id: string;
|
||||
/** An RFC 3339 time string like e.g. '2020-02-15T10:39:10.4696305Z' */
|
||||
readonly time: string;
|
||||
readonly last_commit_hash: string;
|
||||
readonly last_block_id: BlockId;
|
||||
/** Can be empty */
|
||||
readonly data_hash: string;
|
||||
readonly validators_hash: string;
|
||||
readonly next_validators_hash: string;
|
||||
readonly consensus_hash: string;
|
||||
readonly app_hash: string;
|
||||
/** Can be empty */
|
||||
readonly last_results_hash: string;
|
||||
/** Can be empty */
|
||||
readonly evidence_hash: string;
|
||||
readonly proposer_address: string;
|
||||
}
|
||||
export interface Block {
|
||||
readonly header: BlockHeader;
|
||||
readonly data: {
|
||||
/** Array of base64 encoded transactions */
|
||||
readonly txs: ReadonlyArray<string> | null;
|
||||
};
|
||||
}
|
||||
export interface BlockResponse {
|
||||
readonly block_id: BlockId;
|
||||
readonly block: Block;
|
||||
}
|
||||
interface AuthAccountsResponse {
|
||||
readonly height: string;
|
||||
readonly result: {
|
||||
readonly type: "cosmos-sdk/Account";
|
||||
readonly value: CosmosSdkAccount;
|
||||
};
|
||||
}
|
||||
declare type WasmResponse<T = string> = WasmSuccess<T> | WasmError;
|
||||
interface WasmSuccess<T = string> {
|
||||
readonly height: string;
|
||||
readonly result: T;
|
||||
}
|
||||
interface WasmError {
|
||||
readonly error: string;
|
||||
}
|
||||
export interface TxsResponse {
|
||||
readonly height: string;
|
||||
readonly txhash: string;
|
||||
/** 🤷♂️ */
|
||||
readonly codespace?: string;
|
||||
/** Falsy when transaction execution succeeded. Contains error code on error. */
|
||||
readonly code?: number;
|
||||
readonly raw_log: string;
|
||||
readonly logs?: object;
|
||||
readonly tx: CosmosSdkTx;
|
||||
/** The gas limit as set by the user */
|
||||
readonly gas_wanted?: string;
|
||||
/** The gas used by the execution */
|
||||
readonly gas_used?: string;
|
||||
readonly timestamp: string;
|
||||
}
|
||||
interface SearchTxsResponse {
|
||||
readonly total_count: string;
|
||||
readonly count: string;
|
||||
readonly page_number: string;
|
||||
readonly page_total: string;
|
||||
readonly limit: string;
|
||||
readonly txs: readonly TxsResponse[];
|
||||
}
|
||||
export interface PostTxsResponse {
|
||||
readonly height: string;
|
||||
readonly txhash: string;
|
||||
readonly code?: number;
|
||||
readonly raw_log?: string;
|
||||
/** The same as `raw_log` but deserialized? */
|
||||
readonly logs?: object;
|
||||
/** The gas limit as set by the user */
|
||||
readonly gas_wanted?: string;
|
||||
/** The gas used by the execution */
|
||||
readonly gas_used?: string;
|
||||
}
|
||||
interface EncodeTxResponse {
|
||||
readonly tx: string;
|
||||
}
|
||||
export interface CodeInfo {
|
||||
readonly id: number;
|
||||
/** Bech32 account address */
|
||||
readonly creator: string;
|
||||
/** Hex-encoded sha256 hash of the code stored here */
|
||||
readonly data_hash: string;
|
||||
readonly source?: string;
|
||||
readonly builder?: string;
|
||||
}
|
||||
export interface CodeDetails extends CodeInfo {
|
||||
/** Base64 encoded raw wasm data */
|
||||
readonly data: string;
|
||||
}
|
||||
export interface ContractInfo {
|
||||
readonly address: string;
|
||||
readonly code_id: number;
|
||||
/** Bech32 account address */
|
||||
readonly creator: string;
|
||||
readonly label: string;
|
||||
}
|
||||
export interface ContractDetails extends ContractInfo {
|
||||
/** Argument passed on initialization of the contract */
|
||||
readonly init_msg: object;
|
||||
}
|
||||
declare type RestClientResponse =
|
||||
| NodeInfoResponse
|
||||
| BlockResponse
|
||||
| AuthAccountsResponse
|
||||
| TxsResponse
|
||||
| SearchTxsResponse
|
||||
| PostTxsResponse
|
||||
| EncodeTxResponse
|
||||
| WasmResponse<string>
|
||||
| WasmResponse<CodeInfo[]>
|
||||
| WasmResponse<CodeDetails>
|
||||
| WasmResponse<ContractInfo[] | null>
|
||||
| WasmResponse<ContractDetails | null>;
|
||||
/**
|
||||
* The mode used to send transaction
|
||||
*
|
||||
* @see https://cosmos.network/rpc/#/Transactions/post_txs
|
||||
*/
|
||||
export declare enum BroadcastMode {
|
||||
/** Return after tx commit */
|
||||
Block = "block",
|
||||
/** Return afer CheckTx */
|
||||
Sync = "sync",
|
||||
/** Return right away */
|
||||
Async = "async",
|
||||
}
|
||||
export declare class RestClient {
|
||||
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<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>;
|
||||
/** returns the amino-encoding of the transaction performed by the server */
|
||||
encodeTx(tx: CosmosSdkTx): Promise<Uint8Array>;
|
||||
/**
|
||||
* 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>;
|
||||
listCodeInfo(): Promise<readonly CodeInfo[]>;
|
||||
getCode(id: number): Promise<CodeDetails>;
|
||||
listContractsByCodeId(id: number): Promise<readonly ContractInfo[]>;
|
||||
/**
|
||||
* Returns null when contract was not found at this address.
|
||||
*/
|
||||
getContractInfo(address: string): Promise<ContractDetails | null>;
|
||||
getAllContractState(address: string): Promise<readonly Model[]>;
|
||||
queryContractRaw(address: string, key: Uint8Array): Promise<Uint8Array | null>;
|
||||
/**
|
||||
* Makes a smart query on the contract and parses the reponse as JSON.
|
||||
* Throws error if no such contract exists, the query format is invalid or the response is invalid.
|
||||
*/
|
||||
queryContractSmart(address: string, query: object): Promise<JsonObject>;
|
||||
}
|
||||
export {};
|
||||
19
packages/sdk38/types/sequence.d.ts
vendored
Normal file
19
packages/sdk38/types/sequence.d.ts
vendored
Normal file
@ -0,0 +1,19 @@
|
||||
import { CosmosSdkTx } from "./types";
|
||||
/**
|
||||
* Serach for sequence s with `min` <= `s` < `upperBound` to find the sequence that was used to sign the transaction
|
||||
*
|
||||
* @param tx The signed transaction
|
||||
* @param chainId The chain ID for which this transaction was signed
|
||||
* @param accountNumber The account number for which this transaction was signed
|
||||
* @param upperBound The upper bound for the testing, i.e. sequence must be lower than this value
|
||||
* @param min The lowest sequence that is tested
|
||||
*
|
||||
* @returns the sequence if a match was found and undefined otherwise
|
||||
*/
|
||||
export declare function findSequenceForSignedTx(
|
||||
tx: CosmosSdkTx,
|
||||
chainId: string,
|
||||
accountNumber: number,
|
||||
upperBound: number,
|
||||
min?: number,
|
||||
): Promise<number | undefined>;
|
||||
14
packages/sdk38/types/signature.d.ts
vendored
Normal file
14
packages/sdk38/types/signature.d.ts
vendored
Normal file
@ -0,0 +1,14 @@
|
||||
import { StdSignature } from "./types";
|
||||
/**
|
||||
* Takes a binary pubkey and signature to create a signature object
|
||||
*
|
||||
* @param pubkey a compressed secp256k1 public key
|
||||
* @param signature a 64 byte fixed length representation of secp256k1 signature components r and s
|
||||
*/
|
||||
export declare function encodeSecp256k1Signature(pubkey: Uint8Array, signature: Uint8Array): StdSignature;
|
||||
export declare function decodeSignature(
|
||||
signature: StdSignature,
|
||||
): {
|
||||
readonly pubkey: Uint8Array;
|
||||
readonly signature: Uint8Array;
|
||||
};
|
||||
74
packages/sdk38/types/signingcosmosclient.d.ts
vendored
Normal file
74
packages/sdk38/types/signingcosmosclient.d.ts
vendored
Normal file
@ -0,0 +1,74 @@
|
||||
import { Coin } from "./coins";
|
||||
import { Account, CosmosClient, GetNonceResult, PostTxResult } from "./cosmosclient";
|
||||
import { Log } from "./logs";
|
||||
import { BroadcastMode } from "./restclient";
|
||||
import { 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 UploadMeta {
|
||||
/** The source URL */
|
||||
readonly source?: string;
|
||||
/** The builder tag */
|
||||
readonly builder?: string;
|
||||
}
|
||||
export interface UploadResult {
|
||||
/** Size of the original wasm code in bytes */
|
||||
readonly originalSize: number;
|
||||
/** A hex encoded sha256 checksum of the original wasm code (that is stored on chain) */
|
||||
readonly originalChecksum: string;
|
||||
/** Size of the compressed wasm code in bytes */
|
||||
readonly compressedSize: number;
|
||||
/** A hex encoded sha256 checksum of the compressed wasm code (that stored in the transaction) */
|
||||
readonly compressedChecksum: string;
|
||||
/** The ID of the code asigned by the chain */
|
||||
readonly codeId: number;
|
||||
readonly logs: readonly Log[];
|
||||
/** Transaction hash (might be used as transaction ID). Guaranteed to be non-empty upper-case hex */
|
||||
readonly transactionHash: string;
|
||||
}
|
||||
export interface InstantiateResult {
|
||||
/** The address of the newly instantiated contract */
|
||||
readonly contractAddress: string;
|
||||
readonly logs: readonly Log[];
|
||||
/** Transaction hash (might be used as transaction ID). Guaranteed to be non-empty upper-case hex */
|
||||
readonly transactionHash: string;
|
||||
}
|
||||
export interface ExecuteResult {
|
||||
readonly logs: readonly Log[];
|
||||
/** Transaction hash (might be used as transaction ID). Guaranteed to be non-empty upper-case hex */
|
||||
readonly transactionHash: string;
|
||||
}
|
||||
export declare class SigningCosmosClient extends CosmosClient {
|
||||
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(
|
||||
apiUrl: string,
|
||||
senderAddress: string,
|
||||
signCallback: SigningCallback,
|
||||
customFees?: Partial<FeeTable>,
|
||||
broadcastMode?: BroadcastMode,
|
||||
);
|
||||
getNonce(address?: string): Promise<GetNonceResult>;
|
||||
getAccount(address?: string): Promise<Account | undefined>;
|
||||
sendTokens(recipientAddress: string, transferAmount: readonly Coin[], memo?: string): Promise<PostTxResult>;
|
||||
}
|
||||
66
packages/sdk38/types/types.d.ts
vendored
Normal file
66
packages/sdk38/types/types.d.ts
vendored
Normal file
@ -0,0 +1,66 @@
|
||||
import { Coin } from "./coins";
|
||||
/** An Amino/Cosmos SDK StdTx */
|
||||
export interface StdTx {
|
||||
readonly msg: ReadonlyArray<Msg>;
|
||||
readonly fee: StdFee;
|
||||
readonly signatures: ReadonlyArray<StdSignature>;
|
||||
readonly memo: string | undefined;
|
||||
}
|
||||
export declare function isStdTx(txValue: unknown): txValue is StdTx;
|
||||
export interface CosmosSdkTx {
|
||||
readonly type: string;
|
||||
readonly value: StdTx;
|
||||
}
|
||||
interface MsgTemplate {
|
||||
readonly type: string;
|
||||
readonly value: any;
|
||||
}
|
||||
/** A Cosmos SDK token transfer message */
|
||||
export interface MsgSend extends MsgTemplate {
|
||||
readonly type: "cosmos-sdk/MsgSend";
|
||||
readonly value: {
|
||||
/** Bech32 account address */
|
||||
readonly from_address: string;
|
||||
/** Bech32 account address */
|
||||
readonly to_address: string;
|
||||
readonly amount: ReadonlyArray<Coin>;
|
||||
};
|
||||
}
|
||||
export declare type Msg = MsgSend | MsgTemplate;
|
||||
export declare function isMsgSend(msg: Msg): msg is MsgSend;
|
||||
export interface StdFee {
|
||||
readonly amount: ReadonlyArray<Coin>;
|
||||
readonly gas: string;
|
||||
}
|
||||
export interface StdSignature {
|
||||
readonly pub_key: PubKey;
|
||||
readonly signature: string;
|
||||
}
|
||||
export interface PubKey {
|
||||
readonly type: string;
|
||||
readonly value: string;
|
||||
}
|
||||
export declare const pubkeyType: {
|
||||
/** @see https://github.com/tendermint/tendermint/blob/v0.33.0/crypto/ed25519/ed25519.go#L22 */
|
||||
secp256k1: "tendermint/PubKeySecp256k1";
|
||||
/** @see https://github.com/tendermint/tendermint/blob/v0.33.0/crypto/secp256k1/secp256k1.go#L23 */
|
||||
ed25519: "tendermint/PubKeyEd25519";
|
||||
/** @see https://github.com/tendermint/tendermint/blob/v0.33.0/crypto/sr25519/codec.go#L12 */
|
||||
sr25519: "tendermint/PubKeySr25519";
|
||||
};
|
||||
export declare const pubkeyTypes: readonly string[];
|
||||
export interface WasmData {
|
||||
readonly key: string;
|
||||
readonly val: string;
|
||||
}
|
||||
export interface Model {
|
||||
readonly key: Uint8Array;
|
||||
readonly val: Uint8Array;
|
||||
}
|
||||
export declare function parseWasmData({ key, val }: WasmData): Model;
|
||||
/**
|
||||
* An object containing a parsed JSON document. The result of JSON.parse().
|
||||
* This doen't privide any type safety over `any` but expresses intent in the code.
|
||||
*/
|
||||
export declare type JsonObject = any;
|
||||
export {};
|
||||
19
packages/sdk38/webpack.web.config.js
Normal file
19
packages/sdk38/webpack.web.config.js
Normal file
@ -0,0 +1,19 @@
|
||||
const glob = require("glob");
|
||||
const path = require("path");
|
||||
const webpack = require("webpack");
|
||||
|
||||
const target = "web";
|
||||
const distdir = path.join(__dirname, "dist", "web");
|
||||
|
||||
module.exports = [
|
||||
{
|
||||
// bundle used for Karma tests
|
||||
target: target,
|
||||
entry: glob.sync("./build/**/*.spec.js"),
|
||||
output: {
|
||||
path: distdir,
|
||||
filename: "tests.js",
|
||||
},
|
||||
plugins: [new webpack.EnvironmentPlugin(["WASMD_ENABLED"])],
|
||||
},
|
||||
];
|
||||
Loading…
Reference in New Issue
Block a user