Merge pull request #218 from CosmWasm/rm-bcp

Remove BCP package
This commit is contained in:
Simon Warta
2020-06-10 16:14:53 +02:00
committed by GitHub
58 changed files with 5 additions and 3533 deletions
-1
View File
@@ -1 +0,0 @@
../../.eslintignore
-3
View File
@@ -1,3 +0,0 @@
build/
dist/
docs/
-12
View File
@@ -1,12 +0,0 @@
# @iov/cosmos-sdk
[![npm version](https://img.shields.io/npm/v/@iov/cosmos-sdk.svg)](https://www.npmjs.com/package/@iov/cosmos-sdk)
A [BCP](https://github.com/iov-one/iov-core/tree/master/packages/iov-bcp) implementation for Cosmos SDK 0.38.
## License
This package is part of the cosmjs repository, licensed under the Apache
License 2.0 (see
[NOTICE](https://github.com/CosmWasm/cosmjs/blob/master/NOTICE) and
[LICENSE](https://github.com/CosmWasm/cosmjs/blob/master/LICENSE)).
-26
View File
@@ -1,26 +0,0 @@
#!/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();
-47
View File
@@ -1,47 +0,0 @@
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,
});
};
View File
View File
View File
View File
View File
View File
View File
View File
View File
View File
View File
View File
View File
View File
View File
View File
View File
View File
View File
View File
-1
View File
@@ -1 +0,0 @@
Directory used to trigger lerna package updates for all packages
-56
View File
@@ -1,56 +0,0 @@
{
"name": "@iov/cosmos-sdk",
"version": "0.20.0",
"description": "Transaction codec and client to communicate with a Cosmos SDK 0.38 blockchain",
"contributors": [
"Ethan Frey <ethanfrey@users.noreply.github.com>",
"Simon Warta"
],
"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/cosmjs/tree/master/packages/bcp"
},
"private": true,
"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 ChromeHeadless",
"test": "yarn build-or-skip && yarn test-node",
"pack-web": "yarn build-or-skip && webpack --mode development --config webpack.web.config.js"
},
"dependencies": {
"@cosmjs/crypto": "^0.20.0",
"@cosmjs/encoding": "^0.20.0",
"@cosmjs/math": "^0.20.0",
"@cosmjs/sdk38": "^0.20.0",
"@cosmjs/utils": "^0.20.0",
"@iov/bcp": "^2.3.2",
"@iov/stream": "^2.3.2",
"bn.js": "^5.1.1",
"fast-deep-equal": "^3.1.1",
"readonly-date": "^1.0.0",
"xstream": "^11.11.0"
},
"devDependencies": {
"@iov/keycontrol": "^2.3.2",
"@types/bn.js": "^4.11.6"
}
}
-37
View File
@@ -1,37 +0,0 @@
import { fromBase64, fromHex } from "@cosmjs/encoding";
import { Algorithm, PubkeyBytes } from "@iov/bcp";
import { pubkeyToAddress } from "./address";
describe("address", () => {
describe("pubkeyToAddress", () => {
it("works for Secp256k1 compressed", () => {
const prefix = "cosmos";
const pubkey = {
algo: Algorithm.Secp256k1,
data: fromBase64("AtQaCqFnshaZQp6rIkvAPyzThvCvXSDO+9AzbxVErqJP") as PubkeyBytes,
};
expect(pubkeyToAddress(pubkey, prefix)).toEqual("cosmos1h806c7khnvmjlywdrkdgk2vrayy2mmvf9rxk2r");
});
it("works for Secp256k1 uncompressed", () => {
const prefix = "cosmos";
const pubkey = {
algo: Algorithm.Secp256k1,
data: fromBase64(
"BE8EGB7ro1ORuFhjOnZcSgwYlpe0DSFjVNUIkNNQxwKQE7WHpoHoNswYeoFkuYpYSKK4mzFzMV/dB0DVAy4lnNU=",
) as PubkeyBytes,
};
expect(pubkeyToAddress(pubkey, prefix)).toEqual("cosmos1pkptre7fdkl6gfrzlesjjvhxhlc3r4gmmk8rs6");
});
it("works for Ed25519", () => {
const prefix = "cosmos";
const pubkey = {
algo: Algorithm.Ed25519,
data: fromHex("12ee6f581fe55673a1e9e1382a0829e32075a0aa4763c968bc526e1852e78c95") as PubkeyBytes,
};
expect(pubkeyToAddress(pubkey, prefix)).toEqual("cosmos1pfq05em6sfkls66ut4m2257p7qwlk448h8mysz");
});
});
});
-24
View File
@@ -1,24 +0,0 @@
import { Secp256k1 } from "@cosmjs/crypto";
import { toBase64 } from "@cosmjs/encoding";
import { PubKey, pubkeyToAddress as sdkPubkeyToAddress, pubkeyType } from "@cosmjs/sdk38";
import { Address, Algorithm, PubkeyBundle } from "@iov/bcp";
// See https://github.com/tendermint/tendermint/blob/f2ada0a604b4c0763bda2f64fac53d506d3beca7/docs/spec/blockchain/encoding.md#public-key-cryptography
export function pubkeyToAddress(pubkey: PubkeyBundle, prefix: string): Address {
let sdkKey: PubKey;
if (pubkey.algo === Algorithm.Secp256k1) {
sdkKey = {
type: pubkeyType.secp256k1,
value: toBase64(pubkey.data.length > 33 ? Secp256k1.compressPubkey(pubkey.data) : pubkey.data),
};
} else if (pubkey.algo === Algorithm.Ed25519) {
sdkKey = {
type: pubkeyType.ed25519,
value: toBase64(pubkey.data),
};
} else {
throw new Error(`Unsupported algorithm: ${pubkey.algo}`);
}
return sdkPubkeyToAddress(sdkKey, prefix) as Address;
}
-79
View File
@@ -1,79 +0,0 @@
import { ChainId } from "@iov/bcp";
import { Caip5 } from "./caip5";
describe("Caip5", () => {
describe("encode", () => {
it("works for direct format", () => {
expect(Caip5.encode("foo")).toEqual("cosmos:foo");
expect(Caip5.encode("aA1-")).toEqual("cosmos:aA1-");
expect(Caip5.encode("12345678901234567890123456789012345678901234567")).toEqual(
"cosmos:12345678901234567890123456789012345678901234567",
);
// Test vectors from CAIP-5
expect(Caip5.encode("cosmoshub-3")).toEqual("cosmos:cosmoshub-3");
expect(Caip5.encode("Binance-Chain-Tigris")).toEqual("cosmos:Binance-Chain-Tigris");
expect(Caip5.encode("x")).toEqual("cosmos:x");
expect(Caip5.encode("hash-")).toEqual("cosmos:hash-");
expect(Caip5.encode("hashed")).toEqual("cosmos:hashed");
});
it("works for hashed format", () => {
// Test vectors from CAIP-5
expect(Caip5.encode("hashed-")).toEqual("cosmos:hashed-c904589232422def");
expect(Caip5.encode("hashed-123")).toEqual("cosmos:hashed-99df5cd68192b33e");
expect(Caip5.encode("123456789012345678901234567890123456789012345678")).toEqual(
"cosmos:hashed-0204c92a0388779d",
);
expect(Caip5.encode(" ")).toEqual("cosmos:hashed-36a9e7f1c95b82ff");
expect(Caip5.encode("wonderland🧝‍♂️")).toEqual("cosmos:hashed-843d2fc87f40eeb9");
});
it("throws for empty input", () => {
expect(() => Caip5.encode("")).toThrowError(/must not be empty/i);
});
});
describe("decode", () => {
it("works for valid format", () => {
expect(Caip5.decode("cosmos:x" as ChainId)).toEqual("x");
expect(Caip5.decode("cosmos:foo" as ChainId)).toEqual("foo");
expect(Caip5.decode("cosmos:aA1-" as ChainId)).toEqual("aA1-");
expect(Caip5.decode("cosmos:12345678901234567890123456789012345678901234567" as ChainId)).toEqual(
"12345678901234567890123456789012345678901234567",
);
});
it("throws for invalid format", () => {
// wrong namespace
expect(() => Caip5.decode(":foobar" as ChainId)).toThrowError(/not compatible with CAIP-5/i);
expect(() => Caip5.decode("xyz:foobar" as ChainId)).toThrowError(/not compatible with CAIP-5/i);
expect(() => Caip5.decode("cosmos-hash:foobar" as ChainId)).toThrowError(/not compatible with CAIP-5/i);
// reference too short
expect(() => Caip5.decode("cosmos:" as ChainId)).toThrowError(/not compatible with CAIP-5/i);
// reference too long
expect(() =>
Caip5.decode("cosmos:123456789012345678901234567890123456789012345678" as ChainId),
).toThrowError(/not compatible with CAIP-5/i);
// invalid chars
expect(() => Caip5.decode("cosmos:foo bar" as ChainId)).toThrowError(/not compatible with CAIP-5/i);
expect(() => Caip5.decode("cosmos:wonder🧝‍♂️" as ChainId)).toThrowError(/not compatible with CAIP-5/i);
});
it("throws for hashed chain IDs", () => {
expect(() => Caip5.decode("cosmos:hashed-" as ChainId)).toThrowError(
/hashed chain IDs cannot be decoded/i,
);
expect(() => Caip5.decode("cosmos:hashed-abab" as ChainId)).toThrowError(
/hashed chain IDs cannot be decoded/i,
);
expect(() => Caip5.decode("cosmos:hashed-6abb36860ec76c5a" as ChainId)).toThrowError(
/hashed chain IDs cannot be decoded/i,
);
});
});
});
-40
View File
@@ -1,40 +0,0 @@
import { Sha256 } from "@cosmjs/crypto";
import { toHex, toUtf8 } from "@cosmjs/encoding";
import { ChainId } from "@iov/bcp";
const hashedPrefix = "hashed-";
/**
* Conversion between native chain IDs and CAIP-5 format
*
* @see https://github.com/ChainAgnostic/CAIPs/pull/9
*/
export class Caip5 {
/**
* @param native The `chain_id` field from Tendermint's genesis file
*/
public static encode(native: string): ChainId {
if (!native) throw new Error("Input must not be empty");
if (!native.match(/^[-a-zA-Z0-9]{1,47}$/) || native.startsWith(hashedPrefix)) {
const hash = toHex(new Sha256(toUtf8(native)).digest()).slice(0, 16);
return `cosmos:${hashedPrefix}${hash}` as ChainId;
} else {
return `cosmos:${native}` as ChainId;
}
}
public static decode(chainId: ChainId): string {
const match = chainId.match(/^cosmos:([-a-zA-Z0-9]{1,47})$/);
if (!match) {
throw new Error("Chain ID not compatible with CAIP-5");
}
const reference = match[1];
if (reference.startsWith(hashedPrefix)) {
throw new Error("Hashed chain IDs cannot be decoded");
}
return reference;
}
}
-75
View File
@@ -1,75 +0,0 @@
import { toUtf8 } from "@cosmjs/encoding";
import { PostableBytes, PrehashType } from "@iov/bcp";
import { CosmosCodec } from "./cosmoscodec";
import { chainId, nonce, sendTxJson, signedTxBin, signedTxEncodedJson, signedTxJson } from "./testdata.spec";
import { BankToken } from "./types";
const defaultPrefix = "cosmos";
const defaultBankTokens: readonly BankToken[] = [
{
fractionalDigits: 6,
ticker: "ATOM",
denom: "uatom",
},
];
describe("CosmosCodec", () => {
const codec = new CosmosCodec(defaultPrefix, defaultBankTokens);
describe("isValidAddress", () => {
it("accepts valid addresses", () => {
expect(codec.isValidAddress("cosmos1pkptre7fdkl6gfrzlesjjvhxhlc3r4gmmk8rs6")).toEqual(true);
});
it("rejects invalid addresses", () => {
// Bad size
expect(codec.isValidAddress("cosmos10q82zkzzmaku5lazhsvxv7hsg4ntpuhh8289f")).toEqual(false);
// Bad checksum
expect(codec.isValidAddress("cosmos1pkptre7fdkl6gfrzlesjjvhxhlc3r4gmmk8rs7")).toEqual(false);
// Bad prefix
expect(codec.isValidAddress("cosmot10q82zkzzmaku5lazhsvxv7hsg4ntpuhd8j5266")).toEqual(false);
expect(codec.isValidAddress("cosmosvalcons10q82zkzzmaku5lazhsvxv7hsg4ntpuhdwadmss")).toEqual(false);
expect(codec.isValidAddress("cosmosvaloper17mggn4znyeyg25wd7498qxl7r2jhgue8u4qjcq")).toEqual(false);
});
});
describe("bytesToSign", () => {
it("works for SendTransaction via bank module", () => {
const expected = {
bytes: toUtf8(
'{"account_number":"0","chain_id":"cosmoshub-3","fee":{"amount":[{"amount":"2500","denom":"uatom"}],"gas":"100000"},"memo":"","msgs":[{"type":"cosmos-sdk/MsgSend","value":{"amount":[{"amount":"35997500","denom":"uatom"}],"from_address":"cosmos1txqfn5jmcts0x0q7krdxj8tgf98tj0965vqlmq","to_address":"cosmos1nynns8ex9fq6sjjfj8k79ymkdz4sqth06xexae"}}],"sequence":"99"}',
),
prehashType: PrehashType.Sha256,
};
expect(codec.bytesToSign(sendTxJson, nonce)).toEqual(expected);
});
});
describe("bytesToPost", () => {
it("works for SendTransaction via bank module", () => {
const encoded = codec.bytesToPost(signedTxJson);
expect(encoded).toEqual(signedTxEncodedJson);
});
});
describe("parseBytes", () => {
it("throws when trying to decode a transaction without a nonce", () => {
expect(() => codec.parseBytes(signedTxBin as PostableBytes, chainId)).toThrowError(
/nonce is required/i,
);
});
it("properly decodes transactions", () => {
const decoded = codec.parseBytes(signedTxEncodedJson as PostableBytes, chainId, nonce);
expect(decoded).toEqual(signedTxJson);
});
it("round trip works", () => {
const encoded = codec.bytesToPost(signedTxJson);
const decoded = codec.parseBytes(encoded, chainId, nonce);
expect(decoded).toEqual(signedTxJson);
});
});
});
-104
View File
@@ -1,104 +0,0 @@
/* eslint-disable @typescript-eslint/camelcase */
import { Bech32, fromUtf8, toUtf8 } from "@cosmjs/encoding";
import { isStdTx, makeSignBytes, StdTx } from "@cosmjs/sdk38";
import {
Address,
ChainId,
Identity,
Nonce,
PostableBytes,
PrehashType,
SignableBytes,
SignedTransaction,
SigningJob,
TransactionId,
TxCodec,
UnsignedTransaction,
} from "@iov/bcp";
import { pubkeyToAddress } from "./address";
import { Caip5 } from "./caip5";
import { parseSignedTx } from "./decode";
import { buildSignedTx, buildUnsignedTx } from "./encode";
import { BankToken, nonceToAccountNumber, nonceToSequence } from "./types";
function marshalTx(tx: StdTx): Uint8Array {
const json = JSON.stringify(tx);
return toUtf8(json);
}
function unmarshalTx(data: Uint8Array): StdTx {
const decoded = JSON.parse(fromUtf8(data));
if (!isStdTx(decoded)) {
throw new Error("Must be json encoded StdTx");
}
return decoded;
}
export class CosmosCodec implements TxCodec {
private readonly addressPrefix: string;
private readonly bankTokens: readonly BankToken[];
public constructor(addressPrefix: string, bankTokens: readonly BankToken[]) {
this.addressPrefix = addressPrefix;
this.bankTokens = bankTokens;
}
public bytesToSign(unsigned: UnsignedTransaction, nonce: Nonce): SigningJob {
const built = buildUnsignedTx(unsigned, this.bankTokens);
const signBytes = makeSignBytes(
built.value.msg,
built.value.fee,
Caip5.decode(unsigned.chainId),
built.value.memo || "",
nonceToAccountNumber(nonce),
nonceToSequence(nonce),
);
return {
bytes: signBytes as SignableBytes,
prehashType: PrehashType.Sha256,
};
}
// PostableBytes are JSON-encoded StdTx
public bytesToPost(signed: SignedTransaction): PostableBytes {
// TODO: change this as well (return StdTx, not AminoTx)?
const built = buildSignedTx(signed, this.bankTokens);
return marshalTx(built.value) as PostableBytes;
}
// TODO: this needs some marshalling going on...
// Do we need to support this??
public identifier(_signed: SignedTransaction): TransactionId {
throw new Error("Not yet implemented, requires amino encoding- talk to Ethan");
// const bytes = this.bytesToPost(signed);
// const hash = new Sha256(bytes).digest();
// return toHex(hash).toUpperCase() as TransactionId;
}
public parseBytes(bytes: PostableBytes, chainId: ChainId, nonce?: Nonce): SignedTransaction {
if (nonce === undefined) {
throw new Error("Nonce is required");
}
const parsed = unmarshalTx(bytes);
return parseSignedTx(parsed, chainId, nonce, this.bankTokens);
}
public identityToAddress(identity: Identity): Address {
return pubkeyToAddress(identity.pubkey, this.addressPrefix);
}
public isValidAddress(address: string): boolean {
try {
const { prefix, data } = Bech32.decode(address);
if (prefix !== this.addressPrefix) {
return false;
}
return data.length === 20;
} catch {
return false;
}
}
}
-893
View File
@@ -1,893 +0,0 @@
import { Random, Secp256k1, Secp256k1Signature, Sha256 } from "@cosmjs/crypto";
import { Bech32, fromBase64 } from "@cosmjs/encoding";
import { decodeSignature } from "@cosmjs/sdk38";
import { assert } from "@cosmjs/utils";
import {
Account,
Address,
Algorithm,
Amount,
ChainId,
ConfirmedTransaction,
isBlockInfoPending,
isBlockInfoSucceeded,
isConfirmedTransaction,
isSendTransaction,
PubkeyBytes,
SendTransaction,
TokenTicker,
TransactionId,
TransactionState,
UnsignedTransaction,
} from "@iov/bcp";
import { HdPaths, Secp256k1HdWallet, UserProfile } from "@iov/keycontrol";
import BN from "bn.js";
import { CosmosConnection, TokenConfiguration } from "./cosmosconnection";
import { encodeFullSignature } from "./encode";
import * as testdata from "./testdata.spec";
function pendingWithoutWasmd(): void {
if (!process.env.WASMD_ENABLED) {
return pending("Set WASMD_ENABLED to enable Cosmos node-based tests");
}
}
const defaultAddressPrefix = "cosmos";
function makeRandomAddress(): Address {
return Bech32.encode(defaultAddressPrefix, Random.getBytes(20)) as Address;
}
const alice = {
address0: "cosmos14qemq0vw6y3gc3u3e0aty2e764u4gs5le3hada" as Address,
pubkey0: {
algo: Algorithm.Secp256k1,
data: fromBase64("A9cXhWb8ZpqCzkA8dQCPV29KdeRLV3rUYxrkHudLbQtS") as PubkeyBytes,
},
};
/**
* We use a different test account than the faucet, since BCP errors when transactions
* with multiple messages are found.
*/
const bob = {
mnemonic: "remain fragile remove stamp quiz bus country dress critic mammal office need",
path0: HdPaths.cosmosHub(0),
pubkey: {
algo: Algorithm.Secp256k1,
data: fromBase64("A0d/GxY+UALE+miWJP0qyq4/EayG1G6tsg24v+cbD6By") as PubkeyBytes,
},
address: "cosmos1lvrwcvrqlc5ktzp2c4t22xgkx29q3y83lktgzl" as Address,
};
describe("CosmosConnection", () => {
const cosm = "COSM" as TokenTicker;
const httpUrl = "http://localhost:1317";
const defaultChainId = "cosmos:testing" as ChainId;
const defaultEmptyAddress = "cosmos1h806c7khnvmjlywdrkdgk2vrayy2mmvf9rxk2r" as Address;
const defaultRecipient = "cosmos1t70qnpr0az8tf7py83m4ue5y89w58lkjmx0yq2" as Address;
const defaultAmount: Amount = {
quantity: "7744887",
fractionalDigits: 6,
tokenTicker: cosm,
};
const unusedAccount = {
pubkey: {
algo: Algorithm.Secp256k1,
data: fromBase64("ArkCaFUJ/IH+vKBmNRCdUVl3mCAhbopk9jjW4Ko4OfRQ") as PubkeyBytes,
},
address: "cosmos1cjsxept9rkggzxztslae9ndgpdyt2408lk850u" as Address,
};
// this is for wasmd blockchain
const defaultConfig: TokenConfiguration = {
bankTokens: [
{
fractionalDigits: 6,
name: "Fee Token",
ticker: "COSM",
denom: "ucosm",
},
{
fractionalDigits: 6,
name: "Staking Token",
ticker: "STAKE",
denom: "ustake",
},
],
};
const atomConfig: TokenConfiguration = {
bankTokens: [
{
fractionalDigits: 6,
name: "Atom",
ticker: "ATOM",
denom: "uatom",
},
],
};
describe("establish", () => {
it("can connect to Cosmos via http", async () => {
pendingWithoutWasmd();
const connection = await CosmosConnection.establish(httpUrl, defaultAddressPrefix, defaultConfig);
expect(connection).toBeTruthy();
connection.disconnect();
});
});
describe("chainId", () => {
it("displays the chain ID", async () => {
pendingWithoutWasmd();
const connection = await CosmosConnection.establish(httpUrl, defaultAddressPrefix, defaultConfig);
expect(connection.chainId).toEqual(defaultChainId);
connection.disconnect();
});
});
describe("height", () => {
it("displays the current height", async () => {
pendingWithoutWasmd();
const connection = await CosmosConnection.establish(httpUrl, defaultAddressPrefix, defaultConfig);
const height = await connection.height();
expect(height).toBeGreaterThan(0);
connection.disconnect();
});
});
describe("getToken", () => {
it("displays a given token", async () => {
pendingWithoutWasmd();
const connection = await CosmosConnection.establish(httpUrl, defaultAddressPrefix, defaultConfig);
const token = await connection.getToken("COSM" as TokenTicker);
expect(token).toEqual({
fractionalDigits: 6,
tokenName: "Fee Token",
tokenTicker: "COSM" as TokenTicker,
});
connection.disconnect();
});
it("resolves to undefined if the token is not supported", async () => {
pendingWithoutWasmd();
const connection = await CosmosConnection.establish(httpUrl, defaultAddressPrefix, defaultConfig);
const token = await connection.getToken("whatever" as TokenTicker);
expect(token).toBeUndefined();
connection.disconnect();
});
});
describe("getAllTokens", () => {
it("resolves to a list of all supported tokens", async () => {
pendingWithoutWasmd();
const connection = await CosmosConnection.establish(httpUrl, defaultAddressPrefix, defaultConfig);
const tokens = await connection.getAllTokens();
expect(tokens).toEqual([
{
fractionalDigits: 6,
tokenName: "Fee Token",
tokenTicker: "COSM" as TokenTicker,
},
{
fractionalDigits: 6,
tokenName: "Staking Token",
tokenTicker: "STAKE" as TokenTicker,
},
]);
connection.disconnect();
});
});
describe("identifier", () => {
it("calculates tx hash from PostableBytes", async () => {
pendingWithoutWasmd();
const connection = await CosmosConnection.establish(httpUrl, defaultAddressPrefix, atomConfig);
const id = await connection.identifier(testdata.signedTxJson);
expect(id).toMatch(/^[0-9A-F]{64}$/);
expect(id).toEqual(testdata.txId);
});
});
describe("getAccount", () => {
it("gets an empty account by address", async () => {
pendingWithoutWasmd();
const connection = await CosmosConnection.establish(httpUrl, defaultAddressPrefix, defaultConfig);
const account = await connection.getAccount({ address: defaultEmptyAddress });
expect(account).toBeUndefined();
connection.disconnect();
});
it("gets an account by address", async () => {
pendingWithoutWasmd();
const connection = await CosmosConnection.establish(httpUrl, defaultAddressPrefix, defaultConfig);
const account = await connection.getAccount({ address: unusedAccount.address });
assert(account, "Account must be defined");
expect(account.address).toEqual(unusedAccount.address);
expect(account.pubkey).toBeUndefined();
expect(account.balance).toEqual([
{
tokenTicker: "COSM" as TokenTicker,
quantity: "1000000000",
fractionalDigits: 6,
},
{
tokenTicker: "STAKE" as TokenTicker,
quantity: "1000000000",
fractionalDigits: 6,
},
]);
connection.disconnect();
});
it("gets an account by pubkey", async () => {
pendingWithoutWasmd();
const connection = await CosmosConnection.establish(httpUrl, defaultAddressPrefix, defaultConfig);
const byAddress = await connection.getAccount({ address: unusedAccount.address });
const byPubkey = await connection.getAccount({ pubkey: unusedAccount.pubkey });
expect(byPubkey).toEqual(byAddress); // above we verified that by address works as expected
connection.disconnect();
});
it("has a pubkey when getting account with transactions", async () => {
pendingWithoutWasmd();
const connection = await CosmosConnection.establish(httpUrl, defaultAddressPrefix, defaultConfig);
const account = await connection.getAccount({ address: alice.address0 });
expect(account?.pubkey).toEqual(alice.pubkey0);
connection.disconnect();
});
});
describe("watchAccount", () => {
it("can watch account by address", (done) => {
pendingWithoutWasmd();
const recipient = makeRandomAddress();
const events = new Array<Account | undefined>();
(async () => {
const connection = await CosmosConnection.establish(httpUrl, defaultAddressPrefix, defaultConfig);
const subscription = connection.watchAccount({ address: recipient }).subscribe({
next: (event) => {
events.push(event);
if (events.length === 3) {
const [event1, event2, event3] = events;
expect(event1).toBeUndefined();
assert(event2, "Second event must not be undefined");
expect(event2.address).toEqual(recipient);
expect(event2.pubkey).toBeUndefined();
expect(event2.balance.length).toEqual(1);
expect(event2.balance[0].quantity).toEqual(defaultAmount.quantity);
expect(event2.balance[0].tokenTicker).toEqual(defaultAmount.tokenTicker);
assert(event3, "Third event must not be undefined");
expect(event3.address).toEqual(recipient);
expect(event3.pubkey).toBeUndefined();
expect(event3.balance.length).toEqual(1);
expect(event3.balance[0].quantity).toEqual(new BN(defaultAmount.quantity).imuln(2).toString());
expect(event3.balance[0].tokenTicker).toEqual(defaultAmount.tokenTicker);
subscription.unsubscribe();
connection.disconnect();
done();
}
},
complete: done.fail,
error: done.fail,
});
const profile = new UserProfile();
const wallet = profile.addWallet(Secp256k1HdWallet.fromMnemonic(bob.mnemonic));
const sender = await profile.createIdentity(wallet.id, defaultChainId, bob.path0);
const senderAddress = connection.codec.identityToAddress(sender);
for (const i of [0, 1]) {
const sendTx = await connection.withDefaultFee<SendTransaction>({
kind: "bcp/send",
chainId: defaultChainId,
senderPubkey: sender.pubkey,
sender: senderAddress,
recipient: recipient,
amount: defaultAmount,
memo: `Trigger for new event ${i}`,
});
const nonce = await connection.getNonce({ address: senderAddress });
const signedTransaction = await profile.signTransaction(sender, sendTx, connection.codec, nonce);
const result = await connection.postTx(connection.codec.bytesToPost(signedTransaction));
await result.blockInfo.waitFor((info) => !isBlockInfoPending(info));
}
})().catch(done.fail);
});
});
describe("getTx", () => {
it("can get a recently posted bank send transaction", async () => {
pendingWithoutWasmd();
const connection = await CosmosConnection.establish(httpUrl, defaultAddressPrefix, defaultConfig);
const profile = new UserProfile();
const wallet = profile.addWallet(Secp256k1HdWallet.fromMnemonic(bob.mnemonic));
const senderIdentity = await profile.createIdentity(wallet.id, defaultChainId, bob.path0);
const senderAddress = connection.codec.identityToAddress(senderIdentity);
const unsigned = await connection.withDefaultFee<SendTransaction>({
kind: "bcp/send",
chainId: defaultChainId,
sender: senderAddress,
recipient: defaultRecipient,
memo: "My first payment",
amount: {
quantity: "75000",
fractionalDigits: 6,
tokenTicker: cosm,
},
});
const nonce = await connection.getNonce({ address: senderAddress });
const signed = await profile.signTransaction(senderIdentity, unsigned, connection.codec, nonce);
const postableBytes = connection.codec.bytesToPost(signed);
const response = await connection.postTx(postableBytes);
const { transactionId } = response;
await response.blockInfo.waitFor((info) => isBlockInfoSucceeded(info));
const getResponse = await connection.getTx(transactionId);
expect(getResponse.transactionId).toEqual(transactionId);
assert(isConfirmedTransaction(getResponse), "Expected transaction to succeed");
assert(getResponse.log, "Log must be available");
// we get a json response in the log for each msg, multiple events is good (transfer succeeded)
const [firstLog] = JSON.parse(getResponse.log);
expect(firstLog.events.length).toEqual(2);
const { transaction, signatures } = getResponse;
assert(isSendTransaction(transaction), "Expected send transaction");
expect(transaction).toEqual(unsigned);
expect(signatures.length).toEqual(1);
expect(signatures[0]).toEqual({
nonce: signed.signatures[0].nonce,
pubkey: {
algo: signed.signatures[0].pubkey.algo,
data: Secp256k1.compressPubkey(signed.signatures[0].pubkey.data),
},
signature: Secp256k1.trimRecoveryByte(signed.signatures[0].signature),
});
connection.disconnect();
});
it("can get an old transaction", async () => {
pendingWithoutWasmd();
const connection = await CosmosConnection.establish(httpUrl, defaultAddressPrefix, defaultConfig);
const results = await connection.searchTx({ sentFromOrTo: bob.address });
const firstSearchResult = results.find(() => true);
assert(firstSearchResult, "At least one transaction sent by the faucet must be available.");
assert(isConfirmedTransaction(firstSearchResult), "Transaction must be confirmed.");
const {
transaction: searchedTransaction,
transactionId: searchedTransactionId,
height: searchedHeight,
} = firstSearchResult;
const getResponse = await connection.getTx(searchedTransactionId);
assert(isConfirmedTransaction(getResponse), "Expected transaction to succeed");
const { height, transactionId, log, transaction, signatures } = getResponse;
// Test properties of getTx result: height, transactionId, log, transaction
expect(height).toEqual(searchedHeight);
expect(transactionId).toEqual(searchedTransactionId);
assert(log, "Log must be available");
const [firstLog] = JSON.parse(log);
expect(firstLog.events.length).toEqual(2);
expect(transaction).toEqual(searchedTransaction);
// Signature test ensures the nonce is correct
expect(signatures.length).toEqual(1);
const signBytes = connection.codec.bytesToSign(getResponse.transaction, signatures[0].nonce).bytes;
const { pubkey, signature } = decodeSignature(encodeFullSignature(signatures[0]));
const prehashed = new Sha256(signBytes).digest();
const valid = await Secp256k1.verifySignature(
Secp256k1Signature.fromFixedLength(signature),
prehashed,
pubkey,
);
expect(valid).toEqual(true);
connection.disconnect();
});
it("throws for non-existent transaction", async () => {
pendingWithoutWasmd();
const connection = await CosmosConnection.establish(httpUrl, defaultAddressPrefix, defaultConfig);
const nonExistentId = "0000000000000000000000000000000000000000000000000000000000000000" as TransactionId;
await connection.getTx(nonExistentId).then(
() => fail("this must not succeed"),
(error) => expect(error).toMatch(/transaction does not exist/i),
);
connection.disconnect();
});
});
describe("searchTx", () => {
it("can post and search for a transaction", async () => {
pendingWithoutWasmd();
const connection = await CosmosConnection.establish(httpUrl, defaultAddressPrefix, defaultConfig);
const profile = new UserProfile();
const wallet = profile.addWallet(Secp256k1HdWallet.fromMnemonic(bob.mnemonic));
const sender = await profile.createIdentity(wallet.id, defaultChainId, bob.path0);
const senderAddress = connection.codec.identityToAddress(sender);
const unsigned = await connection.withDefaultFee<SendTransaction>({
kind: "bcp/send",
chainId: defaultChainId,
sender: senderAddress,
recipient: defaultRecipient,
memo: "My first payment",
amount: {
quantity: "75000",
fractionalDigits: 6,
tokenTicker: cosm,
},
});
const nonce = await connection.getNonce({ address: senderAddress });
const signed = await profile.signTransaction(sender, unsigned, connection.codec, nonce);
const postableBytes = connection.codec.bytesToPost(signed);
const response = await connection.postTx(postableBytes);
const { transactionId } = response;
const blockInfo = await response.blockInfo.waitFor((info) => !isBlockInfoPending(info));
expect(blockInfo.state).toEqual(TransactionState.Succeeded);
// search by id
const byIdResults = await connection.searchTx({ id: transactionId });
expect(byIdResults.length).toEqual(1);
const byIdResult = byIdResults[0];
expect(byIdResult.transactionId).toEqual(transactionId);
assert(isConfirmedTransaction(byIdResult), "Expected transaction to succeed");
assert(byIdResult.log, "Log must be available");
const [firstByIdlog] = JSON.parse(byIdResult.log);
expect(firstByIdlog.events.length).toEqual(2);
expect(firstByIdlog.events[0].type).toEqual("message");
expect(firstByIdlog.events[1].type).toEqual("transfer");
const byIdTransaction = byIdResult.transaction;
assert(isSendTransaction(byIdTransaction), "Expected send transaction");
expect(byIdTransaction).toEqual(unsigned);
// search by sender address
const bySenderResults = await connection.searchTx({ sentFromOrTo: senderAddress });
expect(bySenderResults).toBeTruthy();
expect(bySenderResults.length).toBeGreaterThanOrEqual(1);
const bySenderResult = bySenderResults[bySenderResults.length - 1];
expect(bySenderResult.transactionId).toEqual(transactionId);
assert(isConfirmedTransaction(bySenderResult), "Expected transaction to succeed");
assert(bySenderResult.log, "Log must be available");
const [firstBySenderLog] = JSON.parse(bySenderResult.log);
expect(firstBySenderLog.events.length).toEqual(2);
expect(firstBySenderLog.events[0].type).toEqual("message");
expect(firstBySenderLog.events[1].type).toEqual("transfer");
const bySenderTransaction = bySenderResult.transaction;
assert(isSendTransaction(bySenderTransaction), "Expected send transaction");
expect(bySenderTransaction).toEqual(unsigned);
// search by recipient address
const byRecipientResults = await connection.searchTx({ sentFromOrTo: defaultRecipient });
expect(byRecipientResults.length).toBeGreaterThanOrEqual(1);
const byRecipientResult = byRecipientResults[byRecipientResults.length - 1];
expect(byRecipientResult.transactionId).toEqual(transactionId);
assert(isConfirmedTransaction(byRecipientResult), "Expected transaction to succeed");
assert(byRecipientResult.log, "Log must be available");
const [firstByRecipientLog] = JSON.parse(bySenderResult.log);
expect(firstByRecipientLog.events.length).toEqual(2);
expect(firstByRecipientLog.events[0].type).toEqual("message");
expect(firstByRecipientLog.events[1].type).toEqual("transfer");
const byRecipeintTransaction = byRecipientResult.transaction;
assert(isSendTransaction(byRecipeintTransaction), "Expected send transaction");
expect(byRecipeintTransaction).toEqual(unsigned);
// search by height
const heightResults = await connection.searchTx({ height: byIdResult.height });
expect(heightResults.length).toBeGreaterThanOrEqual(1);
expect(heightResults).toContain(
jasmine.objectContaining({
transactionId: transactionId,
transaction: unsigned,
}),
);
connection.disconnect();
});
it("can search by minHeight and maxHeight", async () => {
pendingWithoutWasmd();
const connection = await CosmosConnection.establish(httpUrl, defaultAddressPrefix, defaultConfig);
const profile = new UserProfile();
const wallet = profile.addWallet(Secp256k1HdWallet.fromMnemonic(bob.mnemonic));
const sender = await profile.createIdentity(wallet.id, defaultChainId, bob.path0);
const senderAddress = connection.codec.identityToAddress(sender);
const recipient = makeRandomAddress();
const unsigned = await connection.withDefaultFee<SendTransaction>({
kind: "bcp/send",
chainId: defaultChainId,
sender: senderAddress,
recipient: recipient,
memo: "My first payment",
amount: {
quantity: "75000",
fractionalDigits: 6,
tokenTicker: cosm,
},
});
const nonce = await connection.getNonce({ address: senderAddress });
const signed = await profile.signTransaction(sender, unsigned, connection.codec, nonce);
const postableBytes = connection.codec.bytesToPost(signed);
const response = await connection.postTx(postableBytes);
const { transactionId } = response;
const blockInfo = await response.blockInfo.waitFor((info) => !isBlockInfoPending(info));
assert(isBlockInfoSucceeded(blockInfo));
const { height } = blockInfo;
// search by ID
{
const results = await connection.searchTx({ id: transactionId });
expect(results.length).toEqual(1);
}
{
const results = await connection.searchTx({ id: transactionId, minHeight: height });
expect(results.length).toEqual(1);
}
{
const results = await connection.searchTx({ id: transactionId, minHeight: height - 2 });
expect(results.length).toEqual(1);
}
{
const results = await connection.searchTx({ id: transactionId, maxHeight: height });
expect(results.length).toEqual(1);
}
{
const results = await connection.searchTx({ id: transactionId, maxHeight: height + 2 });
expect(results.length).toEqual(1);
}
{
const results = await connection.searchTx({
id: transactionId,
minHeight: height,
maxHeight: height,
});
expect(results.length).toEqual(1);
}
{
const results = await connection.searchTx({ id: transactionId, minHeight: height + 1 });
expect(results.length).toEqual(0);
}
{
const results = await connection.searchTx({ id: transactionId, maxHeight: height - 1 });
expect(results.length).toEqual(0);
}
{
const results = await connection.searchTx({
id: transactionId,
minHeight: height + 1,
maxHeight: Number.MAX_SAFE_INTEGER,
});
expect(results.length).toEqual(0);
}
{
const results = await connection.searchTx({ id: transactionId, minHeight: 0, maxHeight: height - 1 });
expect(results.length).toEqual(0);
}
// search by recipient
{
const results = await connection.searchTx({ sentFromOrTo: recipient });
expect(results.length).toEqual(1);
}
{
const results = await connection.searchTx({ sentFromOrTo: recipient, minHeight: height });
expect(results.length).toEqual(1);
}
{
const results = await connection.searchTx({ sentFromOrTo: recipient, minHeight: height - 2 });
expect(results.length).toEqual(1);
}
{
const results = await connection.searchTx({ sentFromOrTo: recipient, maxHeight: height });
expect(results.length).toEqual(1);
}
{
const results = await connection.searchTx({ sentFromOrTo: recipient, maxHeight: height + 2 });
expect(results.length).toEqual(1);
}
{
const results = await connection.searchTx({ sentFromOrTo: recipient, minHeight: height + 1 });
expect(results.length).toEqual(0);
}
{
const results = await connection.searchTx({ sentFromOrTo: recipient, maxHeight: height - 1 });
expect(results.length).toEqual(0);
}
{
const results = await connection.searchTx({
sentFromOrTo: recipient,
minHeight: height,
maxHeight: height,
});
expect(results.length).toEqual(1);
}
{
const results = await connection.searchTx({ sentFromOrTo: recipient, minHeight: height + 1 });
expect(results.length).toEqual(0);
}
{
const results = await connection.searchTx({ sentFromOrTo: recipient, maxHeight: height - 1 });
expect(results.length).toEqual(0);
}
{
const results = await connection.searchTx({
sentFromOrTo: recipient,
minHeight: height + 1,
maxHeight: Number.MAX_SAFE_INTEGER,
});
expect(results.length).toEqual(0);
}
{
const results = await connection.searchTx({
sentFromOrTo: recipient,
minHeight: 0,
maxHeight: height - 1,
});
expect(results.length).toEqual(0);
}
// search by height
{
const results = await connection.searchTx({ height: height });
expect(results.length).toBeGreaterThanOrEqual(1);
}
{
const results = await connection.searchTx({ height: height, minHeight: height });
expect(results.length).toBeGreaterThanOrEqual(1);
}
{
const results = await connection.searchTx({ height: height, minHeight: height - 2 });
expect(results.length).toBeGreaterThanOrEqual(1);
}
{
const results = await connection.searchTx({ height: height, maxHeight: height });
expect(results.length).toBeGreaterThanOrEqual(1);
}
{
const results = await connection.searchTx({ height: height, maxHeight: height + 2 });
expect(results.length).toBeGreaterThanOrEqual(1);
}
{
const results = await connection.searchTx({ height: height, minHeight: height + 1 });
expect(results.length).toEqual(0);
}
{
const results = await connection.searchTx({ height: height, maxHeight: height - 1 });
expect(results.length).toEqual(0);
}
{
const results = await connection.searchTx({
height: height,
minHeight: height,
maxHeight: height,
});
expect(results.length).toBeGreaterThanOrEqual(1);
}
{
const results = await connection.searchTx({ height: height, minHeight: height + 1 });
expect(results.length).toEqual(0);
}
{
const results = await connection.searchTx({ height: height, maxHeight: height - 1 });
expect(results.length).toEqual(0);
}
{
const results = await connection.searchTx({
height: height,
minHeight: height + 1,
maxHeight: Number.MAX_SAFE_INTEGER,
});
expect(results.length).toEqual(0);
}
{
const results = await connection.searchTx({
height: height,
minHeight: 0,
maxHeight: height - 1,
});
expect(results.length).toEqual(0);
}
connection.disconnect();
});
});
describe("liveTx", () => {
it("can listen to transactions by recipient address (transactions in history and updates)", (done) => {
pendingWithoutWasmd();
(async () => {
const connection = await CosmosConnection.establish(httpUrl, defaultAddressPrefix, defaultConfig);
const profile = new UserProfile();
const wallet = profile.addWallet(Secp256k1HdWallet.fromMnemonic(bob.mnemonic));
const sender = await profile.createIdentity(wallet.id, defaultChainId, bob.path0);
// send transactions
const recipientAddress = makeRandomAddress();
const sendA = await connection.withDefaultFee<SendTransaction>({
kind: "bcp/send",
chainId: defaultChainId,
senderPubkey: sender.pubkey,
sender: connection.codec.identityToAddress(sender),
recipient: recipientAddress,
amount: defaultAmount,
memo: `liveTx() test A ${Math.random()}`,
});
const sendB = await connection.withDefaultFee<SendTransaction>({
kind: "bcp/send",
chainId: defaultChainId,
senderPubkey: sender.pubkey,
sender: connection.codec.identityToAddress(sender),
recipient: recipientAddress,
amount: defaultAmount,
memo: `liveTx() test B ${Math.random()}`,
});
const sendC = await connection.withDefaultFee<SendTransaction>({
kind: "bcp/send",
chainId: defaultChainId,
senderPubkey: sender.pubkey,
sender: connection.codec.identityToAddress(sender),
recipient: recipientAddress,
amount: defaultAmount,
memo: `liveTx() test C ${Math.random()}`,
});
const [nonceA, nonceB, nonceC] = await connection.getNonces({ pubkey: sender.pubkey }, 3);
const signedA = await profile.signTransaction(sender, sendA, connection.codec, nonceA);
const signedB = await profile.signTransaction(sender, sendB, connection.codec, nonceB);
const signedC = await profile.signTransaction(sender, sendC, connection.codec, nonceC);
const bytesToPostA = connection.codec.bytesToPost(signedA);
const bytesToPostB = connection.codec.bytesToPost(signedB);
const bytesToPostC = connection.codec.bytesToPost(signedC);
// Post A and B. Unfortunately the REST server API does not support sending them in parallel because the sequence check fails.
const postResultA = await connection.postTx(bytesToPostA);
await postResultA.blockInfo.waitFor((info) => !isBlockInfoPending(info));
const postResultB = await connection.postTx(bytesToPostB);
await postResultB.blockInfo.waitFor((info) => !isBlockInfoPending(info));
// setup listener after A and B are in block
const events = new Array<ConfirmedTransaction<UnsignedTransaction>>();
const subscription = connection.liveTx({ sentFromOrTo: recipientAddress }).subscribe({
next: (event) => {
assert(isConfirmedTransaction(event), "Confirmed transaction expected");
events.push(event);
assert(isSendTransaction(event.transaction), "Unexpected transaction type");
expect(event.transaction.recipient).toEqual(recipientAddress);
if (events.length === 3) {
expect(events[1].height).toEqual(events[0].height + 1);
expect(events[2].height).toBeGreaterThan(events[1].height);
subscription.unsubscribe();
connection.disconnect();
done();
}
},
});
// Post C
await connection.postTx(bytesToPostC);
})().catch(done.fail);
});
it("can listen to transactions by ID (transaction in history)", (done) => {
pendingWithoutWasmd();
(async () => {
const connection = await CosmosConnection.establish(httpUrl, defaultAddressPrefix, defaultConfig);
const profile = new UserProfile();
const wallet = profile.addWallet(Secp256k1HdWallet.fromMnemonic(bob.mnemonic));
const sender = await profile.createIdentity(wallet.id, defaultChainId, bob.path0);
const recipientAddress = makeRandomAddress();
const send = await connection.withDefaultFee<SendTransaction>({
kind: "bcp/send",
chainId: defaultChainId,
senderPubkey: sender.pubkey,
sender: connection.codec.identityToAddress(sender),
recipient: recipientAddress,
amount: defaultAmount,
memo: `liveTx() test ${Math.random()}`,
});
const nonce = await connection.getNonce({ pubkey: sender.pubkey });
const signed = await profile.signTransaction(sender, send, connection.codec, nonce);
const bytesToPost = connection.codec.bytesToPost(signed);
const postResult = await connection.postTx(bytesToPost);
const transactionId = postResult.transactionId;
// Wait for a block
await postResult.blockInfo.waitFor((info) => !isBlockInfoPending(info));
// setup listener after transaction is in block
const events = new Array<ConfirmedTransaction<UnsignedTransaction>>();
const subscription = connection.liveTx({ id: transactionId }).subscribe({
next: (event) => {
assert(isConfirmedTransaction(event), "Confirmed transaction expected");
events.push(event);
assert(isSendTransaction(event.transaction), "Unexpected transaction type");
expect(event.transaction.recipient).toEqual(recipientAddress);
expect(event.transactionId).toEqual(transactionId);
subscription.unsubscribe();
connection.disconnect();
done();
},
});
})().catch(done.fail);
});
it("can listen to transactions by ID (transaction in updates)", (done) => {
pendingWithoutWasmd();
(async () => {
const connection = await CosmosConnection.establish(httpUrl, defaultAddressPrefix, defaultConfig);
const profile = new UserProfile();
const wallet = profile.addWallet(Secp256k1HdWallet.fromMnemonic(bob.mnemonic));
const sender = await profile.createIdentity(wallet.id, defaultChainId, bob.path0);
// send transactions
const recipientAddress = makeRandomAddress();
const send = await connection.withDefaultFee<SendTransaction>({
kind: "bcp/send",
chainId: defaultChainId,
senderPubkey: sender.pubkey,
sender: connection.codec.identityToAddress(sender),
recipient: recipientAddress,
amount: defaultAmount,
memo: `liveTx() test ${Math.random()}`,
});
const nonce = await connection.getNonce({ pubkey: sender.pubkey });
const signed = await profile.signTransaction(sender, send, connection.codec, nonce);
const bytesToPost = connection.codec.bytesToPost(signed);
const postResult = await connection.postTx(bytesToPost);
const transactionId = postResult.transactionId;
// setup listener before transaction is in block
const events = new Array<ConfirmedTransaction<UnsignedTransaction>>();
const subscription = connection.liveTx({ id: transactionId }).subscribe({
next: (event) => {
assert(isConfirmedTransaction(event), "Confirmed transaction expected");
events.push(event);
assert(isSendTransaction(event.transaction), "Unexpected transaction type");
expect(event.transaction.recipient).toEqual(recipientAddress);
expect(event.transactionId).toEqual(transactionId);
subscription.unsubscribe();
connection.disconnect();
done();
},
});
})().catch(done.fail);
});
});
});
-489
View File
@@ -1,489 +0,0 @@
import { fromUtf8 } from "@cosmjs/encoding";
import { Uint53 } from "@cosmjs/math";
import {
CosmosClient,
findSequenceForSignedTx,
IndexedTx,
isMsgSend,
isStdTx,
SearchTxFilter,
} from "@cosmjs/sdk38";
import {
Account,
AccountQuery,
AddressQuery,
BlockchainConnection,
BlockHeader,
BlockId,
BlockInfo,
ChainId,
ConfirmedAndSignedTransaction,
ConfirmedTransaction,
FailedTransaction,
Fee,
isConfirmedTransaction,
isPubkeyQuery,
isSendTransaction,
Nonce,
PostableBytes,
PostTxResponse,
PubkeyQuery,
SignedTransaction,
Token,
TokenTicker,
TransactionId,
TransactionQuery,
TransactionState,
TxCodec,
UnsignedTransaction,
} from "@iov/bcp";
import { concat, DefaultValueProducer, ValueAndUpdates } from "@iov/stream";
import equal from "fast-deep-equal";
import { ReadonlyDate } from "readonly-date";
import { Producer, Stream } from "xstream";
import { pubkeyToAddress } from "./address";
import { Caip5 } from "./caip5";
import { CosmosCodec } from "./cosmoscodec";
import { decodeAmount, decodePubkey, parseTxsResponseSigned, parseTxsResponseUnsigned } from "./decode";
import { buildSignedTx } from "./encode";
import { accountToNonce, BankToken } from "./types";
// poll every 0.5 seconds (block time 1s)
const defaultPollInterval = 500;
export interface TokenConfiguration {
/** Supported tokens of the Cosmos SDK bank module */
readonly bankTokens: ReadonlyArray<BankToken & { readonly name: string }>;
}
function isDefined<X>(value: X | undefined): value is X {
return value !== undefined;
}
function deduplicate<T>(input: ReadonlyArray<T>, comparator: (a: T, b: T) => number): Array<T> {
const out = new Array<T>();
for (const element of input) {
if (out.find((o) => comparator(o, element) === 0) === undefined) {
out.push(element);
}
}
return out;
}
/** Compares transaxtion by height. If the height is equal, compare by hash to ensure deterministic order */
function compareByHeightAndHash(a: IndexedTx, b: IndexedTx): number {
if (a.height === b.height) {
return a.hash.localeCompare(b.hash);
} else {
return a.height - b.height;
}
}
/** Account and undefined are valid events. The third option means no event fired yet */
type LastWatchAccountEvent = Account | undefined | "no_event_fired_yet";
export class CosmosConnection implements BlockchainConnection {
// we must know prefix and tokens a priori to understand the chain
public static async establish(
url: string,
addressPrefix: string,
tokens: TokenConfiguration,
): Promise<CosmosConnection> {
const cosmosClient = new CosmosClient(url);
const chainData = await this.initialize(cosmosClient);
return new CosmosConnection(cosmosClient, chainData, addressPrefix, tokens);
}
private static async initialize(cosmosClient: CosmosClient): Promise<ChainId> {
const rawChainId = await cosmosClient.getChainId();
return Caip5.encode(rawChainId);
}
public readonly chainId: ChainId;
public readonly codec: TxCodec;
private readonly cosmosClient: CosmosClient;
private readonly addressPrefix: string;
private readonly bankTokens: readonly BankToken[];
// these are derived from arguments (cached for use in multiple functions)
private readonly feeToken: BankToken | undefined;
private readonly supportedTokens: readonly Token[];
private constructor(
cosmosClient: CosmosClient,
chainId: ChainId,
addressPrefix: string,
tokens: TokenConfiguration,
) {
this.cosmosClient = cosmosClient;
this.chainId = chainId;
this.codec = new CosmosCodec(addressPrefix, tokens.bankTokens);
this.addressPrefix = addressPrefix;
this.bankTokens = tokens.bankTokens;
this.feeToken = this.bankTokens.find(() => true);
this.supportedTokens = tokens.bankTokens
.map((info) => ({
tokenTicker: info.ticker as TokenTicker,
tokenName: info.name,
fractionalDigits: info.fractionalDigits,
}))
.sort((a, b) => a.tokenTicker.localeCompare(b.tokenTicker));
}
public disconnect(): void {
return;
}
public async height(): Promise<number> {
return this.cosmosClient.getHeight();
}
public async getToken(searchTicker: TokenTicker): Promise<Token | undefined> {
return (await this.getAllTokens()).find(({ tokenTicker }) => tokenTicker === searchTicker);
}
public async getAllTokens(): Promise<readonly Token[]> {
return this.supportedTokens;
}
/**
* This is a replacement for the unimplemented CosmosCodec.identifier. Here we have more
* context and network available, which we might use to implement the API in an async way.
*/
public async identifier(signed: SignedTransaction): Promise<TransactionId> {
const tx = buildSignedTx(signed, this.bankTokens);
const id = await this.cosmosClient.getIdentifier(tx);
return id as TransactionId;
}
public async getAccount(query: AccountQuery): Promise<Account | undefined> {
const address = isPubkeyQuery(query) ? pubkeyToAddress(query.pubkey, this.addressPrefix) : query.address;
const bankAccount = await this.cosmosClient.getAccount(address);
const supportedBankCoins = (bankAccount?.balance || []).filter(({ denom }) =>
this.bankTokens.find((token) => token.denom === denom),
);
if (!bankAccount) {
return undefined;
} else {
const balance = [
...supportedBankCoins.map((coin) => decodeAmount(this.bankTokens, coin)),
].sort((a, b) => a.tokenTicker.localeCompare(b.tokenTicker));
const pubkey = bankAccount?.pubkey ? decodePubkey(bankAccount.pubkey) : undefined;
return {
address: address,
balance: balance,
pubkey: pubkey,
};
}
}
public watchAccount(query: AccountQuery): Stream<Account | undefined> {
let lastEvent: LastWatchAccountEvent = "no_event_fired_yet";
let pollInternal: NodeJS.Timeout | undefined;
const producer: Producer<Account | undefined> = {
start: async (listener) => {
const poll = async (): Promise<void> => {
try {
const event = await this.getAccount(query);
if (!equal(event, lastEvent)) {
listener.next(event);
lastEvent = event;
}
} catch (error) {
listener.error(error);
}
};
pollInternal = setInterval(poll, defaultPollInterval);
await poll();
},
stop: () => {
if (pollInternal) {
clearInterval(pollInternal);
pollInternal = undefined;
}
},
};
return Stream.create(producer);
}
public async getNonce(query: AddressQuery | PubkeyQuery): Promise<Nonce> {
const address = isPubkeyQuery(query) ? pubkeyToAddress(query.pubkey, this.addressPrefix) : query.address;
const { accountNumber, sequence } = await this.cosmosClient.getNonce(address);
return accountToNonce(accountNumber, sequence);
}
public async getNonces(query: AddressQuery | PubkeyQuery, count: number): Promise<readonly Nonce[]> {
const checkedCount = new Uint53(count).toNumber();
if (checkedCount === 0) {
return [];
}
const firstNonce = await this.getNonce(query);
// Note: this still works with the encoded format (see types/accountToNonce) as least-significant digits are sequence
return [...new Array(checkedCount)].map((_, i) => (firstNonce + i) as Nonce);
}
public async getBlockHeader(height: number): Promise<BlockHeader> {
const { id, header, txs } = await this.cosmosClient.getBlock(height);
return {
id: id as BlockId,
height: header.height,
time: new ReadonlyDate(header.time),
transactionCount: txs.length,
};
}
public watchBlockHeaders(): Stream<BlockHeader> {
throw new Error("not implemented");
}
public async getTx(
id: TransactionId,
): Promise<ConfirmedAndSignedTransaction<UnsignedTransaction> | FailedTransaction> {
const results = await this.cosmosClient.searchTx({ id: id });
switch (results.length) {
case 0:
throw new Error("Transaction does not exist");
case 1:
return this.parseAndPopulateTxResponseSigned(results[0]);
default:
throw new Error("Got unexpected amount of search results");
}
}
public async postTx(tx: PostableBytes): Promise<PostTxResponse> {
const txAsJson = JSON.parse(fromUtf8(tx));
if (!isStdTx(txAsJson)) throw new Error("Postable bytes must contain a JSON encoded StdTx");
const { transactionHash, rawLog } = await this.cosmosClient.postTx(txAsJson);
const transactionId = transactionHash as TransactionId;
const firstEvent: BlockInfo = { state: TransactionState.Pending };
let blockInfoInterval: NodeJS.Timeout;
let lastEventSent: BlockInfo;
const producer = new DefaultValueProducer<BlockInfo>(firstEvent, {
onStarted: () => {
blockInfoInterval = setInterval(async () => {
const searchResult = (await this.searchTx({ id: transactionId })).find(() => true);
if (searchResult) {
const event: BlockInfo = isConfirmedTransaction(searchResult)
? {
state: TransactionState.Succeeded,
height: searchResult.height,
confirmations: searchResult.confirmations,
}
: {
state: TransactionState.Failed,
height: searchResult.height,
code: searchResult.code,
message: searchResult.message,
};
if (!equal(event, lastEventSent)) {
producer.update(event);
lastEventSent = event;
}
}
}, defaultPollInterval);
},
onStop: () => clearInterval(blockInfoInterval),
});
return {
blockInfo: new ValueAndUpdates<BlockInfo>(producer),
transactionId: transactionId,
log: rawLog,
};
}
public async searchTx({
height,
id,
maxHeight,
minHeight,
sentFromOrTo,
signedBy,
tags,
}: TransactionQuery): Promise<readonly (ConfirmedTransaction<UnsignedTransaction> | FailedTransaction)[]> {
if ([signedBy, tags].some(isDefined)) {
throw new Error("Transaction query by signedBy or tags not yet supported");
}
if ([id, height, sentFromOrTo].filter(isDefined).length !== 1) {
throw new Error(
"Transaction query by id, height and sentFromOrTo is mutually exclusive. Exactly one must be set.",
);
}
const filter: SearchTxFilter = { minHeight: minHeight, maxHeight: maxHeight };
let txs: readonly IndexedTx[];
if (id) {
txs = await this.cosmosClient.searchTx({ id: id }, filter);
} else if (height) {
txs = await this.cosmosClient.searchTx({ height: height }, filter);
} else if (sentFromOrTo) {
const pendingRequests = new Array<Promise<readonly IndexedTx[]>>();
pendingRequests.push(this.cosmosClient.searchTx({ sentFromOrTo: sentFromOrTo }, filter));
const responses = await Promise.all(pendingRequests);
const allResults = responses.reduce((accumulator, results) => accumulator.concat(results), []);
txs = deduplicate(allResults, (a, b) => a.hash.localeCompare(b.hash)).sort(compareByHeightAndHash);
} else {
throw new Error("Unsupported query");
}
return txs.map((tx) => this.parseAndPopulateTxResponseUnsigned(tx));
}
public listenTx(
_query: TransactionQuery,
): Stream<ConfirmedTransaction<UnsignedTransaction> | FailedTransaction> {
throw new Error("not implemented");
}
public liveTx(
query: TransactionQuery,
): Stream<ConfirmedTransaction<UnsignedTransaction> | FailedTransaction> {
if ([query.height, query.signedBy, query.tags].some(isDefined)) {
throw new Error("Transaction query by height, signedBy or tags not yet supported");
}
if (query.id) {
if (query.minHeight || query.maxHeight) {
throw new Error("Query by minHeight/maxHeight not supported together with ID");
}
// concat never() because we want non-completing streams consistently
return concat(this.waitForTransaction(query.id), Stream.never());
} else if (query.sentFromOrTo) {
let pollInternal: NodeJS.Timeout | undefined;
const producer: Producer<ConfirmedTransaction<UnsignedTransaction> | FailedTransaction> = {
start: async (listener) => {
let minHeight = query.minHeight || 0;
const maxHeight = query.maxHeight || Number.MAX_SAFE_INTEGER;
const poll = async (): Promise<void> => {
const result = await this.searchTx({
sentFromOrTo: query.sentFromOrTo,
minHeight: minHeight,
maxHeight: maxHeight,
});
for (const item of result) {
listener.next(item);
if (item.height >= minHeight) {
// we assume we got all matching transactions from block `item.height` now
minHeight = item.height + 1;
}
}
};
await poll();
pollInternal = setInterval(poll, defaultPollInterval);
},
stop: () => {
if (pollInternal) {
clearInterval(pollInternal);
pollInternal = undefined;
}
},
};
return Stream.create(producer);
} else {
throw new Error("Unsupported query.");
}
}
public async getFeeQuote(tx: UnsignedTransaction): Promise<Fee> {
if (!isSendTransaction(tx)) {
throw new Error("Received transaction of unsupported kind.");
}
if (!this.feeToken) throw new Error("This connection has no fee token configured.");
return {
tokens: {
fractionalDigits: this.feeToken.fractionalDigits,
quantity: "5000",
tokenTicker: this.feeToken.ticker as TokenTicker,
},
gasLimit: "200000",
};
}
public async withDefaultFee<T extends UnsignedTransaction>(tx: T): Promise<T> {
return {
...tx,
fee: await this.getFeeQuote(tx),
};
}
private parseAndPopulateTxResponseUnsigned(
response: IndexedTx,
): ConfirmedTransaction<UnsignedTransaction> | FailedTransaction {
return parseTxsResponseUnsigned(this.chainId, response.height, response, this.bankTokens);
}
private async parseAndPopulateTxResponseSigned(
response: IndexedTx,
): Promise<ConfirmedAndSignedTransaction<UnsignedTransaction> | FailedTransaction> {
const firstMsg = response.tx.value.msg.find(() => true);
if (!firstMsg) throw new Error("Got transaction without a first message. What is going on here?");
let senderAddress: string;
if (isMsgSend(firstMsg)) {
senderAddress = firstMsg.value.from_address;
} else {
throw new Error(`Got unsupported type of message: ${firstMsg.type}`);
}
const { accountNumber, sequence: currentSequence } = await this.cosmosClient.getNonce(senderAddress);
const sequenceForTx = await findSequenceForSignedTx(
response.tx,
Caip5.decode(this.chainId),
accountNumber,
currentSequence,
);
if (sequenceForTx === undefined) throw new Error("Cound not find matching sequence for this transaction");
const nonce = accountToNonce(accountNumber, sequenceForTx);
return parseTxsResponseSigned(this.chainId, response.height, nonce, response, this.bankTokens);
}
private waitForTransaction(
id: TransactionId,
): Stream<ConfirmedTransaction<UnsignedTransaction> | FailedTransaction> {
let pollInternal: NodeJS.Timeout | undefined;
const producer: Producer<ConfirmedTransaction<UnsignedTransaction> | FailedTransaction> = {
start: (listener) => {
setInterval(async () => {
try {
const results = await this.searchTx({ id: id });
switch (results.length) {
case 0:
// okay, we'll try again
break;
case 1:
listener.next(results[0]);
listener.complete();
break;
default:
throw new Error(`Got unexpected number of search results: ${results.length}`);
}
} catch (error) {
if (pollInternal) {
clearTimeout(pollInternal);
pollInternal = undefined;
}
listener.error(error);
}
}, defaultPollInterval);
},
stop: () => {
if (pollInternal) {
clearTimeout(pollInternal);
pollInternal = undefined;
}
},
};
return Stream.create(producer);
}
}
-21
View File
@@ -1,21 +0,0 @@
import { ChainConnector, ChainId } from "@iov/bcp";
import { CosmosCodec } from "./cosmoscodec";
import { CosmosConnection, TokenConfiguration } from "./cosmosconnection";
/**
* A helper to connect to a cosmos-based chain at a given url
*/
export function createCosmosConnector(
url: string,
addressPrefix: string,
tokenConfig: TokenConfiguration,
expectedChainId?: ChainId,
): ChainConnector<CosmosConnection> {
const codec = new CosmosCodec(addressPrefix, tokenConfig.bankTokens);
return {
establishConnection: async () => CosmosConnection.establish(url, addressPrefix, tokenConfig),
codec: codec,
expectedChainId: expectedChainId,
};
}
-401
View File
@@ -1,401 +0,0 @@
/* eslint-disable @typescript-eslint/camelcase */
import { fromBase64, fromHex } from "@cosmjs/encoding";
import { Coin, IndexedTx, Msg, PubKey, StdSignature } from "@cosmjs/sdk38";
import { Address, Algorithm, SendTransaction, TokenTicker } from "@iov/bcp";
import {
decodeAmount,
decodeFullSignature,
decodePubkey,
decodeSignature,
parseFee,
parseMsg,
parseSignedTx,
parseTxsResponseSigned,
parseTxsResponseUnsigned,
parseUnsignedTx,
} from "./decode";
import * as testdata from "./testdata.spec";
import cosmoshub from "./testdata/cosmoshub.json";
import { BankToken } from "./types";
describe("decode", () => {
const defaultPubkey = {
algo: Algorithm.Secp256k1,
data: fromBase64("AtQaCqFnshaZQp6rIkvAPyzThvCvXSDO+9AzbxVErqJP"),
};
const defaultSignature = fromBase64(
"1nUcIH0CLT0/nQ0mBTDrT6kMG20NY/PsH7P2gc4bpYNGLEYjBmdWevXUJouSE/9A/60QG9cYeqyTe5kFDeIPxQ==",
);
const defaultFullSignature = {
nonce: testdata.nonce,
pubkey: defaultPubkey,
signature: defaultSignature,
};
const defaultAmount = {
fractionalDigits: 6,
quantity: "11657995",
tokenTicker: "ATOM" as TokenTicker,
};
const defaultMemo = "Best greetings";
const defaultSendTransaction: SendTransaction = {
kind: "bcp/send",
chainId: testdata.chainId,
sender: "cosmos1h806c7khnvmjlywdrkdgk2vrayy2mmvf9rxk2r" as Address,
recipient: "cosmos1z7g5w84ynmjyg0kqpahdjqpj7yq34v3suckp0e" as Address,
amount: defaultAmount,
memo: defaultMemo,
};
const defaultFee = {
tokens: {
fractionalDigits: 6,
quantity: "5000",
tokenTicker: "ATOM" as TokenTicker,
},
gasLimit: "200000",
};
const defaultTokens: readonly BankToken[] = [
{
fractionalDigits: 6,
ticker: "ATOM",
denom: "uatom",
},
];
describe("decodePubkey", () => {
it("works for secp256k1", () => {
const pubkey: PubKey = {
type: "tendermint/PubKeySecp256k1",
value: "AtQaCqFnshaZQp6rIkvAPyzThvCvXSDO+9AzbxVErqJP",
};
expect(decodePubkey(pubkey)).toEqual(defaultPubkey);
});
it("works for ed25519", () => {
const pubkey: PubKey = {
type: "tendermint/PubKeyEd25519",
value: "s69CnMgLTpuRyEfecjws3mWssBrOICUx8C2O1DkKSto=",
};
expect(decodePubkey(pubkey)).toEqual({
algo: Algorithm.Ed25519,
data: fromHex("b3af429cc80b4e9b91c847de723c2cde65acb01ace202531f02d8ed4390a4ada"),
});
});
it("throws for unsupported types", () => {
// https://github.com/tendermint/tendermint/blob/v0.33.0/crypto/sr25519/codec.go#L12
const pubkey: PubKey = {
type: "tendermint/PubKeySr25519",
value: "N4FJNPE5r/Twz55kO1QEIxyaGF5/HTXH6WgLQJWsy1o=",
};
expect(() => decodePubkey(pubkey)).toThrowError(/unsupported pubkey type/i);
});
});
describe("decodeSignature", () => {
it("works", () => {
const signature =
"1nUcIH0CLT0/nQ0mBTDrT6kMG20NY/PsH7P2gc4bpYNGLEYjBmdWevXUJouSE/9A/60QG9cYeqyTe5kFDeIPxQ==";
expect(decodeSignature(signature)).toEqual(defaultSignature);
});
});
describe("decodeFullSignature", () => {
it("works", () => {
const fullSignature: StdSignature = {
pub_key: {
type: "tendermint/PubKeySecp256k1",
value: "AtQaCqFnshaZQp6rIkvAPyzThvCvXSDO+9AzbxVErqJP",
},
signature: "1nUcIH0CLT0/nQ0mBTDrT6kMG20NY/PsH7P2gc4bpYNGLEYjBmdWevXUJouSE/9A/60QG9cYeqyTe5kFDeIPxQ==",
};
expect(decodeFullSignature(fullSignature, testdata.nonce)).toEqual(defaultFullSignature);
});
});
describe("decodeAmount", () => {
it("works", () => {
const amount: Coin = {
denom: "uatom",
amount: "11657995",
};
expect(decodeAmount(defaultTokens, amount)).toEqual(defaultAmount);
});
});
describe("parseMsg", () => {
it("works for bank send transaction", () => {
const msg: Msg = {
type: "cosmos-sdk/MsgSend",
value: {
from_address: "cosmos1h806c7khnvmjlywdrkdgk2vrayy2mmvf9rxk2r",
to_address: "cosmos1z7g5w84ynmjyg0kqpahdjqpj7yq34v3suckp0e",
amount: [
{
denom: "uatom",
amount: "11657995",
},
],
},
};
expect(parseMsg(msg, defaultMemo, testdata.chainId, defaultTokens)).toEqual(defaultSendTransaction);
});
});
describe("parseFee", () => {
it("works", () => {
const fee = {
amount: [
{
denom: "uatom",
amount: "5000",
},
],
gas: "200000",
};
expect(parseFee(fee, defaultTokens)).toEqual(defaultFee);
});
});
describe("parseUnsignedTx", () => {
it("works for bank send transaction", () => {
expect(parseUnsignedTx(cosmoshub.tx.value, testdata.chainId, defaultTokens)).toEqual(
testdata.sendTxJson,
);
});
});
describe("parseSignedTx", () => {
it("works", () => {
expect(parseSignedTx(cosmoshub.tx.value, testdata.chainId, testdata.nonce, defaultTokens)).toEqual(
testdata.signedTxJson,
);
});
});
describe("parseTxsResponseUnsigned", () => {
it("works", () => {
const currentHeight = 2923;
const txsResponse: IndexedTx = {
height: 2823,
hash: testdata.txId,
code: 0,
rawLog: '[{"msg_index":0,"success":true,"log":""}]',
logs: [],
tx: cosmoshub.tx,
timestamp: "2020-02-14T11:35:41Z",
};
const expected = {
transaction: testdata.sendTxJson,
height: 2823,
confirmations: 101,
transactionId: testdata.txId,
log: '[{"msg_index":0,"success":true,"log":""}]',
};
expect(parseTxsResponseUnsigned(testdata.chainId, currentHeight, txsResponse, defaultTokens)).toEqual(
expected,
);
});
});
describe("parseTxsResponseSigned", () => {
it("works", () => {
const currentHeight = 2923;
const txsResponse: IndexedTx = {
height: 2823,
hash: testdata.txId,
code: 0,
rawLog: '[{"msg_index":0,"success":true,"log":""}]',
logs: [],
tx: cosmoshub.tx,
timestamp: "2020-02-14T11:35:41Z",
};
const expected = {
...testdata.signedTxJson,
height: 2823,
confirmations: 101,
transactionId: testdata.txId,
log: '[{"msg_index":0,"success":true,"log":""}]',
};
expect(
parseTxsResponseSigned(testdata.chainId, currentHeight, testdata.nonce, txsResponse, defaultTokens),
).toEqual(expected);
});
});
});
/*
Some output from sample rest queries:
$ wasmcli tx send $(wasmcli keys show validator -a) $(wasmcli keys show fred -a) 98765stake -y
{
"height": "4",
"txhash": "8A4613D62884EF8BB9BCCDDA3833D560701908BF17FE82A570EECCBACEF94A91",
"raw_log": "[{\"msg_index\":0,\"log\":\"\",\"events\":[{\"type\":\"message\",\"attributes\":[{\"key\":\"action\",\"value\":\"send\"},{\"key\":\"sender\",\"value\":\"cosmos16qu479grzwanyzav6xvtzncgdjkwhqw7vy2pje\"},{\"key\":\"module\",\"value\":\"bank\"}]},{\"type\":\"transfer\",\"attributes\":[{\"key\":\"recipient\",\"value\":\"cosmos1ltkhnmdcqemmd2tkhnx7qx66tq7e0wykw2j85k\"},{\"key\":\"amount\",\"value\":\"98765stake\"}]}]}]",
"logs": [
{
"msg_index": 0,
"log": "",
"events": [
{
"type": "message",
"attributes": [
{
"key": "action",
"value": "send"
},
{
"key": "sender",
"value": "cosmos16qu479grzwanyzav6xvtzncgdjkwhqw7vy2pje"
},
{
"key": "module",
"value": "bank"
}
]
},
{
"type": "transfer",
"attributes": [
{
"key": "recipient",
"value": "cosmos1ltkhnmdcqemmd2tkhnx7qx66tq7e0wykw2j85k"
},
{
"key": "amount",
"value": "98765stake"
}
]
}
]
}
],
"gas_wanted": "200000",
"gas_used": "53254"
}
$ wasmcli query tx 8A4613D62884EF8BB9BCCDDA3833D560701908BF17FE82A570EECCBACEF94A91
{
"height": "4",
"txhash": "8A4613D62884EF8BB9BCCDDA3833D560701908BF17FE82A570EECCBACEF94A91",
"raw_log": "[{\"msg_index\":0,\"log\":\"\",\"events\":[{\"type\":\"message\",\"attributes\":[{\"key\":\"action\",\"value\":\"send\"},{\"key\":\"sender\",\"value\":\"cosmos16qu479grzwanyzav6xvtzncgdjkwhqw7vy2pje\"},{\"key\":\"module\",\"value\":\"bank\"}]},{\"type\":\"transfer\",\"attributes\":[{\"key\":\"recipient\",\"value\":\"cosmos1ltkhnmdcqemmd2tkhnx7qx66tq7e0wykw2j85k\"},{\"key\":\"amount\",\"value\":\"98765stake\"}]}]}]",
"logs": [
{
"msg_index": 0,
"log": "",
"events": [
{
"type": "message",
"attributes": [
{
"key": "action",
"value": "send"
},
{
"key": "sender",
"value": "cosmos16qu479grzwanyzav6xvtzncgdjkwhqw7vy2pje"
},
{
"key": "module",
"value": "bank"
}
]
},
{
"type": "transfer",
"attributes": [
{
"key": "recipient",
"value": "cosmos1ltkhnmdcqemmd2tkhnx7qx66tq7e0wykw2j85k"
},
{
"key": "amount",
"value": "98765stake"
}
]
}
]
}
],
"gas_wanted": "200000",
"gas_used": "53254",
"tx": {
"type": "cosmos-sdk/StdTx",
"value": {
"msg": [
{
"type": "cosmos-sdk/MsgSend",
"value": {
"from_address": "cosmos16qu479grzwanyzav6xvtzncgdjkwhqw7vy2pje",
"to_address": "cosmos1ltkhnmdcqemmd2tkhnx7qx66tq7e0wykw2j85k",
"amount": [
{
"denom": "stake",
"amount": "98765"
}
]
}
}
],
"fee": {
"amount": [],
"gas": "200000"
},
"signatures": [
{
"pub_key": {
"type": "tendermint/PubKeySecp256k1",
"value": "A11L8EitFnA6YsZ2QSnbMNmK+qI2kxyevDtSfhPqOwcp"
},
"signature": "qCeKoqZeaL0LThKrUXHLgu72jwTiF+DseSBjcKHtcONE0kIdybwYJpuYg3Jj71hmfync+daHNdqgJlPRma0pPA=="
}
],
"memo": ""
}
},
"timestamp": "2020-02-03T17:06:58Z"
}
$ wasmcli query account $(wasmcli keys show fred -a)
{
"type": "cosmos-sdk/Account",
"value": {
"address": "cosmos1ltkhnmdcqemmd2tkhnx7qx66tq7e0wykw2j85k",
"coins": [
{
"denom": "stake",
"amount": "98765"
}
],
"public_key": "",
"account_number": 7,
"sequence": 0
}
}
$ wasmcli query account $(wasmcli keys show validator -a)
{
"type": "cosmos-sdk/Account",
"value": {
"address": "cosmos16qu479grzwanyzav6xvtzncgdjkwhqw7vy2pje",
"coins": [
{
"denom": "stake",
"amount": "899901235"
},
{
"denom": "validatortoken",
"amount": "1000000000"
}
],
"public_key": "cosmospub1addwnpepqdw5huzg45t8qwnzcemyz2wmxrvc474zx6f3e84u8df8uyl28vrjjnp9v4p",
"account_number": 3,
"sequence": 2
}
}
*/
-187
View File
@@ -1,187 +0,0 @@
import { fromBase64 } from "@cosmjs/encoding";
import { Decimal } from "@cosmjs/math";
import {
Coin,
IndexedTx,
isMsgSend,
isStdTx,
Msg,
PubKey,
pubkeyType,
StdFee,
StdSignature,
StdTx,
} from "@cosmjs/sdk38";
import {
Address,
Algorithm,
Amount,
ChainId,
ConfirmedAndSignedTransaction,
ConfirmedTransaction,
Fee,
FullSignature,
Nonce,
PubkeyBundle,
PubkeyBytes,
SendTransaction,
SignatureBytes,
SignedTransaction,
TokenTicker,
TransactionId,
UnsignedTransaction,
} from "@iov/bcp";
import { BankToken } from "./types";
export function decodePubkey(pubkey: PubKey): PubkeyBundle {
switch (pubkey.type) {
case pubkeyType.secp256k1:
return {
algo: Algorithm.Secp256k1,
data: fromBase64(pubkey.value) as PubkeyBytes,
};
case pubkeyType.ed25519:
return {
algo: Algorithm.Ed25519,
data: fromBase64(pubkey.value) as PubkeyBytes,
};
default:
throw new Error("Unsupported pubkey type");
}
}
export function decodeSignature(signature: string): SignatureBytes {
return fromBase64(signature) as SignatureBytes;
}
export function decodeFullSignature(signature: StdSignature, nonce: number): FullSignature {
return {
nonce: nonce as Nonce,
pubkey: decodePubkey(signature.pub_key),
signature: decodeSignature(signature.signature),
};
}
export function coinToDecimal(tokens: readonly BankToken[], coin: Coin): readonly [Decimal, string] {
const match = tokens.find(({ denom }) => denom === coin.denom);
if (!match) {
throw Error(`unknown denom: ${coin.denom}`);
}
const value = Decimal.fromAtomics(coin.amount, match.fractionalDigits);
return [value, match.ticker];
}
export function decodeAmount(tokens: readonly BankToken[], coin: Coin): Amount {
const [value, ticker] = coinToDecimal(tokens, coin);
return {
quantity: value.atomics,
fractionalDigits: value.fractionalDigits,
tokenTicker: ticker as TokenTicker,
};
}
export function parseMsg(
msg: Msg,
memo: string | undefined,
chainId: ChainId,
tokens: readonly BankToken[],
): UnsignedTransaction {
if (isMsgSend(msg)) {
if (msg.value.amount.length !== 1) {
throw new Error("Only MsgSend with one amount is supported");
}
const send: SendTransaction = {
kind: "bcp/send",
chainId: chainId,
sender: msg.value.from_address as Address,
recipient: msg.value.to_address as Address,
amount: decodeAmount(tokens, msg.value.amount[0]),
memo: memo,
};
return send;
} else {
// Unknown transaction type
const unknown = {
chainId: chainId,
kind: "bcp/unknown",
};
return unknown;
}
}
export function parseFee(fee: StdFee, tokens: readonly BankToken[]): Fee {
if (fee.amount.length !== 1) {
throw new Error("Only fee with one amount is supported");
}
return {
tokens: decodeAmount(tokens, fee.amount[0]),
gasLimit: fee.gas,
};
}
export function parseUnsignedTx(
txValue: StdTx,
chainId: ChainId,
tokens: readonly BankToken[],
): UnsignedTransaction {
if (!isStdTx(txValue)) {
throw new Error("Only StdTx is supported");
}
if (txValue.msg.length !== 1) {
throw new Error("Only single-message transactions currently supported");
}
const msg = parseMsg(txValue.msg[0], txValue.memo, chainId, tokens);
const fee = parseFee(txValue.fee, tokens);
return {
...msg,
chainId: chainId,
fee: fee,
};
}
export function parseSignedTx(
txValue: StdTx,
chainId: ChainId,
nonce: Nonce,
tokens: readonly BankToken[],
): SignedTransaction {
const [primarySignature] = txValue.signatures.map((signature) => decodeFullSignature(signature, nonce));
return {
transaction: parseUnsignedTx(txValue, chainId, tokens),
signatures: [primarySignature],
};
}
export function parseTxsResponseUnsigned(
chainId: ChainId,
currentHeight: number,
response: IndexedTx,
tokens: readonly BankToken[],
): ConfirmedTransaction<UnsignedTransaction> {
return {
transaction: parseUnsignedTx(response.tx.value, chainId, tokens),
height: response.height,
confirmations: currentHeight - response.height + 1,
transactionId: response.hash as TransactionId,
log: response.rawLog,
};
}
export function parseTxsResponseSigned(
chainId: ChainId,
currentHeight: number,
nonce: Nonce,
response: IndexedTx,
tokens: readonly BankToken[],
): ConfirmedAndSignedTransaction<UnsignedTransaction> {
return {
...parseSignedTx(response.tx.value, chainId, nonce, tokens),
height: response.height,
confirmations: currentHeight - response.height + 1,
transactionId: response.hash as TransactionId,
log: response.rawLog,
};
}
-310
View File
@@ -1,310 +0,0 @@
/* eslint-disable @typescript-eslint/camelcase */
import { fromBase64 } from "@cosmjs/encoding";
import {
Address,
Algorithm,
Amount,
ChainId,
Nonce,
PubkeyBytes,
SendTransaction,
SignatureBytes,
SignedTransaction,
TokenTicker,
} from "@iov/bcp";
import {
buildSignedTx,
buildUnsignedTx,
encodeFee,
encodeFullSignature,
encodePubkey,
toBankCoin,
} from "./encode";
import { BankToken } from "./types";
describe("encode", () => {
const atom = "ATOM" as TokenTicker;
// https://rpc.cosmos.network:26657/tx?hash=0x2268EB5AB730B45F8426078827BB5BB49819CE2B0D74B2C1D191070BADB379F1&prove=true
const defaultPubkey = {
algo: Algorithm.Secp256k1,
data: fromBase64("AtQaCqFnshaZQp6rIkvAPyzThvCvXSDO+9AzbxVErqJP") as PubkeyBytes,
};
const defaultChainId = "not-used" as ChainId;
const defaultSender = "cosmos1h806c7khnvmjlywdrkdgk2vrayy2mmvf9rxk2r" as Address;
const defaultRecipient = "cosmos1z7g5w84ynmjyg0kqpahdjqpj7yq34v3suckp0e" as Address;
const defaultAmount: Amount = {
fractionalDigits: 6,
quantity: "11657995",
tokenTicker: atom,
};
const defaultMemo = "hello cosmos hub";
const defaultTokens: readonly BankToken[] = [
{
fractionalDigits: 6,
ticker: "ATOM",
denom: "uatom",
},
];
describe("encodePubkey", () => {
it("works for compressed public key", () => {
expect(encodePubkey(defaultPubkey)).toEqual({
type: "tendermint/PubKeySecp256k1",
value: "AtQaCqFnshaZQp6rIkvAPyzThvCvXSDO+9AzbxVErqJP",
});
});
});
describe("toBankCoin", () => {
it("encodes an amount", () => {
expect(toBankCoin(defaultAmount, defaultTokens)).toEqual({
denom: "uatom",
amount: "11657995",
});
});
});
describe("encodeFee", () => {
it("throws without tokens", () => {
const fee = {
gasLimit: "200000",
};
expect(() => encodeFee(fee, defaultTokens)).toThrowError(/cannot encode fee without tokens/i);
});
it("throws without gas limit", () => {
const fee = {
tokens: {
fractionalDigits: 6,
quantity: "5000",
tokenTicker: atom,
},
};
expect(() => encodeFee(fee, defaultTokens)).toThrowError(/cannot encode fee without gas limit/i);
});
it("encodes a fee", () => {
const fee = {
tokens: {
fractionalDigits: 6,
quantity: "5000",
tokenTicker: atom,
},
gasLimit: "200000",
};
expect(encodeFee(fee, defaultTokens)).toEqual({
amount: [{ denom: "uatom", amount: "5000" }],
gas: "200000",
});
});
});
describe("encodeFullSignature", () => {
it("encodes a full signature", () => {
const signature = {
nonce: 0 as Nonce,
pubkey: {
algo: Algorithm.Secp256k1,
data: fromBase64("AtQaCqFnshaZQp6rIkvAPyzThvCvXSDO+9AzbxVErqJP") as PubkeyBytes,
},
signature: fromBase64(
"1nUcIH0CLT0/nQ0mBTDrT6kMG20NY/PsH7P2gc4bpYNGLEYjBmdWevXUJouSE/9A/60QG9cYeqyTe5kFDeIPxQ==",
) as SignatureBytes,
};
expect(encodeFullSignature(signature)).toEqual({
pub_key: {
type: "tendermint/PubKeySecp256k1",
value: "AtQaCqFnshaZQp6rIkvAPyzThvCvXSDO+9AzbxVErqJP",
},
signature: "1nUcIH0CLT0/nQ0mBTDrT6kMG20NY/PsH7P2gc4bpYNGLEYjBmdWevXUJouSE/9A/60QG9cYeqyTe5kFDeIPxQ==",
});
});
it("compresses uncompressed public keys", () => {
const signature = {
nonce: 0 as Nonce,
pubkey: {
algo: Algorithm.Secp256k1,
data: fromBase64(
"BE8EGB7ro1ORuFhjOnZcSgwYlpe0DSFjVNUIkNNQxwKQE7WHpoHoNswYeoFkuYpYSKK4mzFzMV/dB0DVAy4lnNU=",
) as PubkeyBytes,
},
signature: fromBase64(
"1nUcIH0CLT0/nQ0mBTDrT6kMG20NY/PsH7P2gc4bpYNGLEYjBmdWevXUJouSE/9A/60QG9cYeqyTe5kFDeIPxQ==",
) as SignatureBytes,
};
expect(encodeFullSignature(signature)).toEqual({
pub_key: {
type: "tendermint/PubKeySecp256k1",
value: "A08EGB7ro1ORuFhjOnZcSgwYlpe0DSFjVNUIkNNQxwKQ",
},
signature: "1nUcIH0CLT0/nQ0mBTDrT6kMG20NY/PsH7P2gc4bpYNGLEYjBmdWevXUJouSE/9A/60QG9cYeqyTe5kFDeIPxQ==",
});
});
it("removes recovery values from signature data", () => {
const signature = {
nonce: 0 as Nonce,
pubkey: {
algo: Algorithm.Secp256k1,
data: fromBase64("AtQaCqFnshaZQp6rIkvAPyzThvCvXSDO+9AzbxVErqJP") as PubkeyBytes,
},
signature: Uint8Array.from([
...fromBase64(
"1nUcIH0CLT0/nQ0mBTDrT6kMG20NY/PsH7P2gc4bpYNGLEYjBmdWevXUJouSE/9A/60QG9cYeqyTe5kFDeIPxQ==",
),
99,
]) as SignatureBytes,
};
expect(encodeFullSignature(signature)).toEqual({
pub_key: {
type: "tendermint/PubKeySecp256k1",
value: "AtQaCqFnshaZQp6rIkvAPyzThvCvXSDO+9AzbxVErqJP",
},
signature: "1nUcIH0CLT0/nQ0mBTDrT6kMG20NY/PsH7P2gc4bpYNGLEYjBmdWevXUJouSE/9A/60QG9cYeqyTe5kFDeIPxQ==",
});
});
});
describe("buildUnsignedTx", () => {
it("throws for unsupported transaction", () => {
const tx = {
kind: "bns/return_escrow",
chainId: defaultChainId,
escrowId: "defg",
};
expect(() => buildUnsignedTx(tx, defaultTokens)).toThrowError(
/received transaction of unsupported kind/i,
);
});
it("throws for a send transaction without fee", () => {
// This will be rejected by the REST server. Better throw early to avoid hard to debug errors.
const tx = {
kind: "bcp/send",
chainId: defaultChainId,
amount: defaultAmount,
sender: defaultSender,
recipient: defaultRecipient,
memo: defaultMemo,
};
expect(() => buildUnsignedTx(tx, defaultTokens)).toThrowError(/transaction fee must be set/i);
});
it("builds a send transaction with fee", () => {
const tx = {
kind: "bcp/send",
chainId: defaultChainId,
amount: defaultAmount,
sender: defaultSender,
recipient: defaultRecipient,
memo: defaultMemo,
fee: {
tokens: {
fractionalDigits: 6,
quantity: "5000",
tokenTicker: atom,
},
gasLimit: "200000",
},
};
expect(buildUnsignedTx(tx, defaultTokens)).toEqual({
type: "cosmos-sdk/StdTx",
value: {
msg: [
{
type: "cosmos-sdk/MsgSend",
value: {
from_address: "cosmos1h806c7khnvmjlywdrkdgk2vrayy2mmvf9rxk2r",
to_address: "cosmos1z7g5w84ynmjyg0kqpahdjqpj7yq34v3suckp0e",
amount: [
{
denom: "uatom",
amount: "11657995",
},
],
},
},
],
fee: {
amount: [{ denom: "uatom", amount: "5000" }],
gas: "200000",
},
signatures: [],
memo: defaultMemo,
},
});
});
});
describe("buildSignedTx", () => {
it("builds a send transaction", () => {
const tx: SignedTransaction<SendTransaction> = {
transaction: {
kind: "bcp/send",
chainId: defaultChainId,
amount: defaultAmount,
sender: defaultSender,
recipient: defaultRecipient,
memo: defaultMemo,
fee: {
tokens: {
fractionalDigits: 6,
quantity: "5000",
tokenTicker: atom,
},
gasLimit: "200000",
},
},
signatures: [
{
nonce: 0 as Nonce,
pubkey: {
algo: Algorithm.Secp256k1,
data: fromBase64("AtQaCqFnshaZQp6rIkvAPyzThvCvXSDO+9AzbxVErqJP") as PubkeyBytes,
},
signature: fromBase64(
"1nUcIH0CLT0/nQ0mBTDrT6kMG20NY/PsH7P2gc4bpYNGLEYjBmdWevXUJouSE/9A/60QG9cYeqyTe5kFDeIPxQ==",
) as SignatureBytes,
},
],
};
expect(buildSignedTx(tx, defaultTokens)).toEqual({
type: "cosmos-sdk/StdTx",
value: {
msg: [
{
type: "cosmos-sdk/MsgSend",
value: {
from_address: "cosmos1h806c7khnvmjlywdrkdgk2vrayy2mmvf9rxk2r",
to_address: "cosmos1z7g5w84ynmjyg0kqpahdjqpj7yq34v3suckp0e",
amount: [
{
denom: "uatom",
amount: "11657995",
},
],
},
},
],
fee: {
amount: [{ denom: "uatom", amount: "5000" }],
gas: "200000",
},
signatures: [
{
pub_key: {
type: "tendermint/PubKeySecp256k1",
value: "AtQaCqFnshaZQp6rIkvAPyzThvCvXSDO+9AzbxVErqJP",
},
signature:
"1nUcIH0CLT0/nQ0mBTDrT6kMG20NY/PsH7P2gc4bpYNGLEYjBmdWevXUJouSE/9A/60QG9cYeqyTe5kFDeIPxQ==",
},
],
memo: defaultMemo,
},
});
});
});
});
-123
View File
@@ -1,123 +0,0 @@
/* eslint-disable @typescript-eslint/camelcase */
import { Secp256k1 } from "@cosmjs/crypto";
import { toBase64 } from "@cosmjs/encoding";
import {
Coin,
CosmosSdkTx,
encodeSecp256k1Pubkey,
encodeSecp256k1Signature,
PubKey,
pubkeyType,
StdFee,
StdSignature,
} from "@cosmjs/sdk38";
import {
Algorithm,
Amount,
Fee,
FullSignature,
isSendTransaction,
PubkeyBundle,
SignedTransaction,
UnsignedTransaction,
} from "@iov/bcp";
import { BankToken } from "./types";
// TODO: This function seems to be unused and is not well tested (e.g. uncompressed secp256k1 or ed25519)
export function encodePubkey(pubkey: PubkeyBundle): PubKey {
switch (pubkey.algo) {
case Algorithm.Secp256k1:
return encodeSecp256k1Pubkey(pubkey.data);
case Algorithm.Ed25519:
return {
type: pubkeyType.ed25519,
value: toBase64(pubkey.data),
};
default:
throw new Error("Unsupported pubkey algo");
}
}
export function toBankCoin(amount: Amount, tokens: readonly BankToken[]): Coin {
const match = tokens.find((token) => token.ticker === amount.tokenTicker);
if (!match) throw Error(`unknown ticker: ${amount.tokenTicker}`);
if (match.fractionalDigits !== amount.fractionalDigits) {
throw new Error(
"Mismatch in fractional digits between token and value. If you really want, implement a conversion here. However, this indicates a bug in the caller code.",
);
}
return {
denom: match.denom,
amount: amount.quantity,
};
}
export function encodeFee(fee: Fee, tokens: readonly BankToken[]): StdFee {
if (fee.tokens === undefined) {
throw new Error("Cannot encode fee without tokens");
}
if (fee.gasLimit === undefined) {
throw new Error("Cannot encode fee without gas limit");
}
return {
amount: [toBankCoin(fee.tokens, tokens)],
gas: fee.gasLimit,
};
}
export function encodeFullSignature(fullSignature: FullSignature): StdSignature {
switch (fullSignature.pubkey.algo) {
case Algorithm.Secp256k1: {
const compressedPubkey = Secp256k1.compressPubkey(fullSignature.pubkey.data);
const normalizedSignature = Secp256k1.trimRecoveryByte(fullSignature.signature);
return encodeSecp256k1Signature(compressedPubkey, normalizedSignature);
}
default:
throw new Error("Unsupported signing algorithm");
}
}
export function buildUnsignedTx(tx: UnsignedTransaction, bankTokens: readonly BankToken[]): CosmosSdkTx {
if (!isSendTransaction(tx)) {
throw new Error("Received transaction of unsupported kind");
}
const matchingBankToken = bankTokens.find((t) => t.ticker === tx.amount.tokenTicker);
if (!tx.fee) throw new Error("Transaction fee must be set");
if (matchingBankToken) {
return {
type: "cosmos-sdk/StdTx",
value: {
msg: [
{
type: "cosmos-sdk/MsgSend",
value: {
from_address: tx.sender,
to_address: tx.recipient,
amount: [toBankCoin(tx.amount, bankTokens)],
},
},
],
memo: tx.memo || "",
signatures: [],
fee: encodeFee(tx.fee, bankTokens),
},
};
} else {
throw new Error("Cannot encode this type of transaction");
}
}
export function buildSignedTx(tx: SignedTransaction, bankTokens: readonly BankToken[]): CosmosSdkTx {
const built = buildUnsignedTx(tx.transaction, bankTokens);
return {
...built,
value: {
...built.value,
signatures: tx.signatures.map(encodeFullSignature),
},
};
}
-4
View File
@@ -1,4 +0,0 @@
export { CosmosCodec } from "./cosmoscodec";
export { CosmosConnection, TokenConfiguration } from "./cosmosconnection";
export { createCosmosConnector } from "./cosmosconnector";
export { BankToken } from "./types";
-66
View File
@@ -1,66 +0,0 @@
import { fromBase64, toUtf8 } from "@cosmjs/encoding";
import {
Address,
Algorithm,
ChainId,
FullSignature,
Nonce,
PubkeyBundle,
PubkeyBytes,
SendTransaction,
SignatureBytes,
SignedTransaction,
TokenTicker,
TransactionId,
} from "@iov/bcp";
import data from "./testdata/cosmoshub.json";
export const pubJson: PubkeyBundle = {
algo: Algorithm.Secp256k1,
data: fromBase64(data.tx.value.signatures[0].pub_key.value) as PubkeyBytes,
};
export const chainId = "cosmos:cosmoshub-3" as ChainId;
export const nonce = 99 as Nonce;
export const sendTxJson: SendTransaction = {
kind: "bcp/send",
chainId: chainId,
sender: data.tx.value.msg[0].value.from_address as Address,
recipient: data.tx.value.msg[0].value.to_address as Address,
memo: data.tx.value.memo,
amount: {
fractionalDigits: 6,
quantity: data.tx.value.msg[0].value.amount[0].amount,
tokenTicker: "ATOM" as TokenTicker,
},
fee: {
tokens: {
fractionalDigits: 6,
quantity: data.tx.value.fee.amount[0].amount,
tokenTicker: "ATOM" as TokenTicker,
},
gasLimit: data.tx.value.fee.gas,
},
};
export const signedTxSig: FullSignature = {
nonce: nonce,
pubkey: pubJson,
signature: fromBase64(data.tx.value.signatures[0].signature) as SignatureBytes,
};
export const signedTxJson: SignedTransaction = {
transaction: sendTxJson,
signatures: [signedTxSig],
};
export const signedTxBin = fromBase64(data.tx_data);
export const txId = data.id as TransactionId;
export const signedTxEncodedJson = toUtf8(
`{"msg":[{"type":"cosmos-sdk/MsgSend","value":{"from_address":"cosmos1txqfn5jmcts0x0q7krdxj8tgf98tj0965vqlmq","to_address":"cosmos1nynns8ex9fq6sjjfj8k79ymkdz4sqth06xexae","amount":[{"denom":"uatom","amount":"35997500"}]}}],"memo":"","signatures":[{"pub_key":{"type":"tendermint/PubKeySecp256k1","value":"A5qFcJBJvEK/fOmEAY0DHNWwSRZ9TEfNZyH8VoVvDtAq"},"signature":"NK1Oy4EUGAsoC03c1wi9GG03JC/39LEdautC5Jk643oIbEPqeXHMwaqbdvO/Jws0X/NAXaN8SAy2KNY5Qml+5Q=="}],"fee":{"amount":[{"denom":"uatom","amount":"2500"}],"gas":"100000"}}`,
);
-44
View File
@@ -1,44 +0,0 @@
{
"//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"
}
-15
View File
@@ -1,15 +0,0 @@
/* eslint-disable @typescript-eslint/camelcase */
import { accountToNonce, nonceToAccountNumber, nonceToSequence } from "./types";
describe("nonceEncoding", () => {
it("works for input in range", () => {
const nonce = accountToNonce(1234, 7890);
expect(nonceToAccountNumber(nonce)).toEqual(1234);
expect(nonceToSequence(nonce)).toEqual(7890);
});
it("errors on input too large", () => {
expect(() => accountToNonce(1234567890, 7890)).toThrow();
expect(() => accountToNonce(178, 97320247923)).toThrow();
});
});
-52
View File
@@ -1,52 +0,0 @@
import { Nonce } from "@iov/bcp";
export interface BankToken {
readonly denom: string;
readonly ticker: string;
/**
* The number of fractional digits the token supports.
*
* A quantity is expressed as atomic units. 10^fractionalDigits of those
* atomic units make up 1 token.
*
* E.g. in Ethereum 10^18 wei are 1 ETH and from the quantity 123000000000000000000
* the last 18 digits are the fractional part and the rest the wole part.
*/
readonly fractionalDigits: number;
}
// eslint-disable-next-line no-bitwise
const maxAcct = 1 << 23;
// eslint-disable-next-line no-bitwise
const maxSeq = 1 << 20;
// this (lossily) encodes the two pieces of info (uint64) needed to sign into
// one (53-bit) number. Cross your fingers.
export function accountToNonce(accountNumber: number, sequence: number): Nonce {
// we allow 23 bits (8 million) for accounts, and 20 bits (1 million) for tx/account
// let's fix this soon
if (accountNumber > maxAcct) {
throw new Error("Account number is greater than 2^23, must update Nonce handler");
}
if (sequence > maxSeq) {
throw new Error("Sequence is greater than 2^20, must update Nonce handler");
}
const val = accountNumber * maxSeq + sequence;
return val as Nonce;
}
// this extracts info from nonce for signing
export function nonceToAccountNumber(nonce: Nonce): number {
const acct = nonce / maxSeq;
if (acct > maxAcct) {
throw new Error("Invalid Nonce, account number is higher than can safely be encoded in Nonce");
}
return Math.round(acct);
}
// this extracts info from nonce for signing
export function nonceToSequence(nonce: Nonce): number {
const seq = nonce % maxSeq;
return Math.round(seq);
}
-12
View File
@@ -1,12 +0,0 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"baseUrl": ".",
"outDir": "build",
"declarationDir": "build/types",
"rootDir": "src"
},
"include": [
"src/**/*"
]
}
-14
View File
@@ -1,14 +0,0 @@
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,
};
-2
View File
@@ -1,2 +0,0 @@
import { Address, PubkeyBundle } from "@iov/bcp";
export declare function pubkeyToAddress(pubkey: PubkeyBundle, prefix: string): Address;
-13
View File
@@ -1,13 +0,0 @@
import { ChainId } from "@iov/bcp";
/**
* Conversion between native chain IDs and CAIP-5 format
*
* @see https://github.com/ChainAgnostic/CAIPs/pull/9
*/
export declare class Caip5 {
/**
* @param native The `chain_id` field from Tendermint's genesis file
*/
static encode(native: string): ChainId;
static decode(chainId: ChainId): string;
}
-24
View File
@@ -1,24 +0,0 @@
import {
Address,
ChainId,
Identity,
Nonce,
PostableBytes,
SignedTransaction,
SigningJob,
TransactionId,
TxCodec,
UnsignedTransaction,
} from "@iov/bcp";
import { BankToken } from "./types";
export declare class CosmosCodec implements TxCodec {
private readonly addressPrefix;
private readonly bankTokens;
constructor(addressPrefix: string, bankTokens: readonly BankToken[]);
bytesToSign(unsigned: UnsignedTransaction, nonce: Nonce): SigningJob;
bytesToPost(signed: SignedTransaction): PostableBytes;
identifier(_signed: SignedTransaction): TransactionId;
parseBytes(bytes: PostableBytes, chainId: ChainId, nonce?: Nonce): SignedTransaction;
identityToAddress(identity: Identity): Address;
isValidAddress(address: string): boolean;
}
-78
View File
@@ -1,78 +0,0 @@
import {
Account,
AccountQuery,
AddressQuery,
BlockchainConnection,
BlockHeader,
ChainId,
ConfirmedAndSignedTransaction,
ConfirmedTransaction,
FailedTransaction,
Fee,
Nonce,
PostableBytes,
PostTxResponse,
PubkeyQuery,
SignedTransaction,
Token,
TokenTicker,
TransactionId,
TransactionQuery,
TxCodec,
UnsignedTransaction,
} from "@iov/bcp";
import { Stream } from "xstream";
import { BankToken } from "./types";
export interface TokenConfiguration {
/** Supported tokens of the Cosmos SDK bank module */
readonly bankTokens: ReadonlyArray<
BankToken & {
readonly name: string;
}
>;
}
export declare class CosmosConnection implements BlockchainConnection {
static establish(url: string, addressPrefix: string, tokens: TokenConfiguration): Promise<CosmosConnection>;
private static initialize;
readonly chainId: ChainId;
readonly codec: TxCodec;
private readonly cosmosClient;
private readonly addressPrefix;
private readonly bankTokens;
private readonly feeToken;
private readonly supportedTokens;
private constructor();
disconnect(): void;
height(): Promise<number>;
getToken(searchTicker: TokenTicker): Promise<Token | undefined>;
getAllTokens(): Promise<readonly Token[]>;
/**
* This is a replacement for the unimplemented CosmosCodec.identifier. Here we have more
* context and network available, which we might use to implement the API in an async way.
*/
identifier(signed: SignedTransaction): Promise<TransactionId>;
getAccount(query: AccountQuery): Promise<Account | undefined>;
watchAccount(query: AccountQuery): Stream<Account | undefined>;
getNonce(query: AddressQuery | PubkeyQuery): Promise<Nonce>;
getNonces(query: AddressQuery | PubkeyQuery, count: number): Promise<readonly Nonce[]>;
getBlockHeader(height: number): Promise<BlockHeader>;
watchBlockHeaders(): Stream<BlockHeader>;
getTx(id: TransactionId): Promise<ConfirmedAndSignedTransaction<UnsignedTransaction> | FailedTransaction>;
postTx(tx: PostableBytes): Promise<PostTxResponse>;
searchTx({
height,
id,
maxHeight,
minHeight,
sentFromOrTo,
signedBy,
tags,
}: TransactionQuery): Promise<readonly (ConfirmedTransaction<UnsignedTransaction> | FailedTransaction)[]>;
listenTx(_query: TransactionQuery): Stream<ConfirmedTransaction<UnsignedTransaction> | FailedTransaction>;
liveTx(query: TransactionQuery): Stream<ConfirmedTransaction<UnsignedTransaction> | FailedTransaction>;
getFeeQuote(tx: UnsignedTransaction): Promise<Fee>;
withDefaultFee<T extends UnsignedTransaction>(tx: T): Promise<T>;
private parseAndPopulateTxResponseUnsigned;
private parseAndPopulateTxResponseSigned;
private waitForTransaction;
}
-11
View File
@@ -1,11 +0,0 @@
import { ChainConnector, ChainId } from "@iov/bcp";
import { CosmosConnection, TokenConfiguration } from "./cosmosconnection";
/**
* A helper to connect to a cosmos-based chain at a given url
*/
export declare function createCosmosConnector(
url: string,
addressPrefix: string,
tokenConfig: TokenConfiguration,
expectedChainId?: ChainId,
): ChainConnector<CosmosConnection>;
-52
View File
@@ -1,52 +0,0 @@
import { Decimal } from "@cosmjs/math";
import { Coin, IndexedTx, Msg, PubKey, StdFee, StdSignature, StdTx } from "@cosmjs/sdk38";
import {
Amount,
ChainId,
ConfirmedAndSignedTransaction,
ConfirmedTransaction,
Fee,
FullSignature,
Nonce,
PubkeyBundle,
SignatureBytes,
SignedTransaction,
UnsignedTransaction,
} from "@iov/bcp";
import { BankToken } from "./types";
export declare function decodePubkey(pubkey: PubKey): PubkeyBundle;
export declare function decodeSignature(signature: string): SignatureBytes;
export declare function decodeFullSignature(signature: StdSignature, nonce: number): FullSignature;
export declare function coinToDecimal(tokens: readonly BankToken[], coin: Coin): readonly [Decimal, string];
export declare function decodeAmount(tokens: readonly BankToken[], coin: Coin): Amount;
export declare function parseMsg(
msg: Msg,
memo: string | undefined,
chainId: ChainId,
tokens: readonly BankToken[],
): UnsignedTransaction;
export declare function parseFee(fee: StdFee, tokens: readonly BankToken[]): Fee;
export declare function parseUnsignedTx(
txValue: StdTx,
chainId: ChainId,
tokens: readonly BankToken[],
): UnsignedTransaction;
export declare function parseSignedTx(
txValue: StdTx,
chainId: ChainId,
nonce: Nonce,
tokens: readonly BankToken[],
): SignedTransaction;
export declare function parseTxsResponseUnsigned(
chainId: ChainId,
currentHeight: number,
response: IndexedTx,
tokens: readonly BankToken[],
): ConfirmedTransaction<UnsignedTransaction>;
export declare function parseTxsResponseSigned(
chainId: ChainId,
currentHeight: number,
nonce: Nonce,
response: IndexedTx,
tokens: readonly BankToken[],
): ConfirmedAndSignedTransaction<UnsignedTransaction>;
-12
View File
@@ -1,12 +0,0 @@
import { Coin, CosmosSdkTx, PubKey, StdFee, StdSignature } from "@cosmjs/sdk38";
import { Amount, Fee, FullSignature, PubkeyBundle, SignedTransaction, UnsignedTransaction } from "@iov/bcp";
import { BankToken } from "./types";
export declare function encodePubkey(pubkey: PubkeyBundle): PubKey;
export declare function toBankCoin(amount: Amount, tokens: readonly BankToken[]): Coin;
export declare function encodeFee(fee: Fee, tokens: readonly BankToken[]): StdFee;
export declare function encodeFullSignature(fullSignature: FullSignature): StdSignature;
export declare function buildUnsignedTx(
tx: UnsignedTransaction,
bankTokens: readonly BankToken[],
): CosmosSdkTx;
export declare function buildSignedTx(tx: SignedTransaction, bankTokens: readonly BankToken[]): CosmosSdkTx;
-4
View File
@@ -1,4 +0,0 @@
export { CosmosCodec } from "./cosmoscodec";
export { CosmosConnection, TokenConfiguration } from "./cosmosconnection";
export { createCosmosConnector } from "./cosmosconnector";
export { BankToken } from "./types";
-18
View File
@@ -1,18 +0,0 @@
import { Nonce } from "@iov/bcp";
export interface BankToken {
readonly denom: string;
readonly ticker: string;
/**
* The number of fractional digits the token supports.
*
* A quantity is expressed as atomic units. 10^fractionalDigits of those
* atomic units make up 1 token.
*
* E.g. in Ethereum 10^18 wei are 1 ETH and from the quantity 123000000000000000000
* the last 18 digits are the fractional part and the rest the wole part.
*/
readonly fractionalDigits: number;
}
export declare function accountToNonce(accountNumber: number, sequence: number): Nonce;
export declare function nonceToAccountNumber(nonce: Nonce): number;
export declare function nonceToSequence(nonce: Nonce): number;
-19
View File
@@ -1,19 +0,0 @@
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"])],
},
];
+5 -164
View File
@@ -92,71 +92,6 @@
unique-filename "^1.1.1"
which "^1.3.1"
"@iov/bcp@^2.3.2":
version "2.3.2"
resolved "https://registry.yarnpkg.com/@iov/bcp/-/bcp-2.3.2.tgz#23dc92839951beb239ec6ed56490d7534d67451c"
integrity sha512-o9ae5Q8jHgGyxsUnyoRcL200YVbDEYXAb6ZaAFad5/aFjhVawjTDSgya+vL8zI+t8BBixJZcXOjeDaqLtWRQAw==
dependencies:
"@iov/crypto" "^2.3.2"
"@iov/encoding" "^2.3.2"
"@iov/stream" "^2.3.2"
type-tagger "^1.0.0"
xstream "^11.10.0"
"@iov/crypto@^2.3.2":
version "2.3.2"
resolved "https://registry.yarnpkg.com/@iov/crypto/-/crypto-2.3.2.tgz#0f65623d29d1df7e35620bbbeede70f1d0797ff9"
integrity sha512-inHHZa9kr+4K88ZhqOQC/LVlZBcZlE0Kkj97ynWGzaR4CCdCFa0KOkx2vef2CH0J6wbfC13e6b93/MuQyGiUAw==
dependencies:
"@iov/encoding" "^2.3.2"
bip39 "^3.0.2"
bn.js "^4.11.8"
elliptic "^6.4.0"
js-sha3 "^0.8.0"
libsodium-wrappers "^0.7.6"
pbkdf2 "^3.0.16"
ripemd160 "^2.0.2"
sha.js "^2.4.11"
type-tagger "^1.0.0"
unorm "^1.5.0"
"@iov/encoding@^2.3.2":
version "2.3.2"
resolved "https://registry.yarnpkg.com/@iov/encoding/-/encoding-2.3.2.tgz#4b37966af0345a6bc904bb58189dc1ea9d14ad9b"
integrity sha512-viioqo1flTkG4Oxb0PvoBXGozHq9fObAgAL4dRHJe9zmChE77EBX2Y5u0nabd2JwAhEbir56AtsrUe4dOrtd5w==
dependencies:
base64-js "^1.3.0"
bech32 "^1.1.4"
bn.js "^4.11.8"
readonly-date "^1.0.0"
"@iov/keycontrol@^2.3.2":
version "2.3.2"
resolved "https://registry.yarnpkg.com/@iov/keycontrol/-/keycontrol-2.3.2.tgz#42b556476c489e45b01ec47ec9ed28f287f5199e"
integrity sha512-Ile1c+ceAwefulNZ2pYu1TFeoFcx4S2oJTS57sFDNJzDDjn5xvEgNpdn4xkDDdGvkQOnexiMNifeCjInHBJpjQ==
dependencies:
"@iov/bcp" "^2.3.2"
"@iov/crypto" "^2.3.2"
"@iov/encoding" "^2.3.2"
"@iov/stream" "^2.3.2"
"@types/abstract-leveldown" "^5.0.1"
"@types/levelup" "^3.1.0"
"@types/node" "^10.12.18"
"@types/random-js" "^1.0.31"
levelup "^4.0.0"
long "^4.0.0"
random-js "^1.0.8"
readonly-date "^1.0.0"
type-tagger "^1.0.0"
xstream "^11.10.0"
"@iov/stream@^2.3.2":
version "2.3.2"
resolved "https://registry.yarnpkg.com/@iov/stream/-/stream-2.3.2.tgz#472063f3a4fcd1e97de0ae99189f98b94825afac"
integrity sha512-nOq5OKwK2rWnyzXpGTAKZToCU0jmTjEC05owVzmBfw3+7hmjlExrGGbLncjcMWzbvp2VVpHc2l01Tr+qpzr8Vw==
dependencies:
xstream "^11.10.0"
"@koa/cors@^3.0.0":
version "3.0.0"
resolved "https://registry.yarnpkg.com/@koa/cors/-/cors-3.0.0.tgz#df021b4df2dadf1e2b04d7c8ddf93ba2d42519cb"
@@ -955,11 +890,6 @@
dependencies:
"@types/node" ">= 8"
"@types/abstract-leveldown@*", "@types/abstract-leveldown@^5.0.1":
version "5.0.1"
resolved "https://registry.yarnpkg.com/@types/abstract-leveldown/-/abstract-leveldown-5.0.1.tgz#3c7750d0186b954c7f2d2f6acc8c3c7ba0c3412e"
integrity sha512-wYxU3kp5zItbxKmeRYCEplS2MW7DzyBnxPGj+GJVHZEUZiK/nn5Ei1sUFgURDh+X051+zsGe28iud3oHjrYWQQ==
"@types/accepts@*":
version "1.3.5"
resolved "https://registry.yarnpkg.com/@types/accepts/-/accepts-1.3.5.tgz#c34bec115cfc746e04fe5a059df4ce7e7b391575"
@@ -1122,14 +1052,6 @@
dependencies:
"@types/koa" "*"
"@types/levelup@^3.1.0":
version "3.1.1"
resolved "https://registry.yarnpkg.com/@types/levelup/-/levelup-3.1.1.tgz#f7cc08f248f14cb6c92914e91bceb8761020e8f0"
integrity sha512-LjvlfctJYj23Xuqq3jCT8ZPSUSSgDcRJg8+XFDBasoYzefFbB4cHzlDmBVjc2rBOYvklpYHJRayD0jBsbJLD9w==
dependencies:
"@types/abstract-leveldown" "*"
"@types/node" "*"
"@types/libsodium-wrappers@^0.7.7":
version "0.7.7"
resolved "https://registry.yarnpkg.com/@types/libsodium-wrappers/-/libsodium-wrappers-0.7.7.tgz#cdb25e85458612ec80f0157c3815fac187d0b6d2"
@@ -1155,11 +1077,6 @@
resolved "https://registry.yarnpkg.com/@types/node/-/node-11.11.6.tgz#df929d1bb2eee5afdda598a41930fe50b43eaa6a"
integrity sha512-Exw4yUWMBXM3X+8oqzJNRqZSwUAaS4+7NdvHqQuFi/d+synz++xmX3QIf+BFqneW8N31R8Ky+sikfZUXq07ggQ==
"@types/node@^10.12.18":
version "10.17.17"
resolved "https://registry.yarnpkg.com/@types/node/-/node-10.17.17.tgz#7a183163a9e6ff720d86502db23ba4aade5999b8"
integrity sha512-gpNnRnZP3VWzzj5k3qrpRC6Rk3H/uclhAVo1aIvwzK5p5cOrs9yEyQ8H/HBsBY0u5rrWxXEiVPQ0dEB6pkjE8Q==
"@types/pako@^1.0.1":
version "1.0.1"
resolved "https://registry.yarnpkg.com/@types/pako/-/pako-1.0.1.tgz#33b237f3c9aff44d0f82fe63acffa4a365ef4a61"
@@ -1172,11 +1089,6 @@
dependencies:
"@types/node" "*"
"@types/random-js@^1.0.31":
version "1.0.31"
resolved "https://registry.yarnpkg.com/@types/random-js/-/random-js-1.0.31.tgz#18a8bcc075afa504421e638fcbe021f27e802941"
integrity sha512-EAM56DrKw3VhcE4HV0/YlVKeJI07We4Mz1ra6TNtZpaMoiBVMA2bkLEcoFpYOyxoDXfVZWojxkR617LTqtRI0A==
"@types/range-parser@*":
version "1.2.3"
resolved "https://registry.yarnpkg.com/@types/range-parser/-/range-parser-1.2.3.tgz#7ee330ba7caafb98090bece86a5ee44115904c2c"
@@ -1442,15 +1354,6 @@ abbrev@1:
resolved "https://registry.yarnpkg.com/abbrev/-/abbrev-1.1.1.tgz#f8f2c887ad10bf67f634f005b6987fed3179aac8"
integrity sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==
abstract-leveldown@~6.2.1:
version "6.2.2"
resolved "https://registry.yarnpkg.com/abstract-leveldown/-/abstract-leveldown-6.2.2.tgz#677425beeb28204367c7639e264e93ea4b49971a"
integrity sha512-/a+Iwj0rn//CX0EJOasNyZJd2o8xur8Ce9C57Sznti/Ilt/cb6Qd8/k98A4ZOklXgTG+iAYYUs1OTG0s1eH+zQ==
dependencies:
level-concat-iterator "~2.0.0"
level-supports "~1.0.0"
xtend "~4.0.0"
accepts@^1.3.5, accepts@~1.3.4:
version "1.3.7"
resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.7.tgz#531bc726517a3b2b41f850021c6cc15eaab507cd"
@@ -1906,11 +1809,6 @@ bn.js@^4.0.0, bn.js@^4.1.0, bn.js@^4.1.1, bn.js@^4.11.8, bn.js@^4.4.0:
resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-4.11.8.tgz#2cde09eb5ee341f484746bb0309b3253b1b1442f"
integrity sha512-ItfYfPLkWHUjckQCk8xC+LwxgK8NYcXywGigJgSwOP8Y2iyWT4f2vsZnoOXTTbo+o5yXmIUJ4gn5538SO5S3gA==
bn.js@^5.1.1:
version "5.1.1"
resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-5.1.1.tgz#48efc4031a9c4041b9c99c6941d903463ab62eb5"
integrity sha512-IUTD/REb78Z2eodka1QZyyEk66pciRcP6Sroka0aI3tG/iwIdYLrBD62RsubR7vqdt3WyX8p4jxeatzmRSphtA==
body-parser@^1.19.0:
version "1.19.0"
resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.19.0.tgz#96b2709e57c9c4e09a6fd66a8fd979844f69f08a"
@@ -2834,14 +2732,6 @@ defaults@^1.0.3:
dependencies:
clone "^1.0.2"
deferred-leveldown@~5.3.0:
version "5.3.0"
resolved "https://registry.yarnpkg.com/deferred-leveldown/-/deferred-leveldown-5.3.0.tgz#27a997ad95408b61161aa69bd489b86c71b78058"
integrity sha512-a59VOT+oDy7vtAbLRCZwWgxu2BaCfd5Hk7wxJd48ei7I+nsg8Orlb9CLG0PMZienk9BSUKgeAqkO2+Lw+1+Ukw==
dependencies:
abstract-leveldown "~6.2.1"
inherits "^2.0.3"
define-properties@^1.1.2, define-properties@^1.1.3:
version "1.1.3"
resolved "https://registry.yarnpkg.com/define-properties/-/define-properties-1.1.3.tgz#cf88da6cbee26fe6db7094f61d870cbd84cee9f1"
@@ -3155,7 +3045,7 @@ err-code@^1.0.0:
resolved "https://registry.yarnpkg.com/err-code/-/err-code-1.1.2.tgz#06e0116d3028f6aef4806849eb0ea6a748ae6960"
integrity sha1-BuARbTAo9q70gGhJ6w6mp0iuaWA=
errno@^0.1.3, errno@~0.1.1, errno@~0.1.7:
errno@^0.1.3, errno@~0.1.7:
version "0.1.7"
resolved "https://registry.yarnpkg.com/errno/-/errno-0.1.7.tgz#4684d71779ad39af177e3f007996f7c67c852618"
integrity sha512-MfrRBDWzIWifgq6tJj60gkAwtLNb6sQPlcFrSOflcP1aFmmruKQ2wRnze/8V6kgyz7H3FF8Npzv78mZ7XLLflg==
@@ -4311,7 +4201,7 @@ inflight@^1.0.4:
once "^1.3.0"
wrappy "1"
inherits@2, inherits@2.0.4, inherits@^2.0.1, inherits@^2.0.3, inherits@^2.0.4, inherits@~2.0.1, inherits@~2.0.3:
inherits@2, inherits@2.0.4, inherits@^2.0.1, inherits@^2.0.3, inherits@~2.0.1, inherits@~2.0.3:
version "2.0.4"
resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c"
integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==
@@ -4967,45 +4857,6 @@ lerna@^3.20.2:
import-local "^2.0.0"
npmlog "^4.1.2"
level-concat-iterator@~2.0.0:
version "2.0.1"
resolved "https://registry.yarnpkg.com/level-concat-iterator/-/level-concat-iterator-2.0.1.tgz#1d1009cf108340252cb38c51f9727311193e6263"
integrity sha512-OTKKOqeav2QWcERMJR7IS9CUo1sHnke2C0gkSmcR7QuEtFNLLzHQAvnMw8ykvEcv0Qtkg0p7FOwP1v9e5Smdcw==
level-errors@~2.0.0:
version "2.0.1"
resolved "https://registry.yarnpkg.com/level-errors/-/level-errors-2.0.1.tgz#2132a677bf4e679ce029f517c2f17432800c05c8"
integrity sha512-UVprBJXite4gPS+3VznfgDSU8PTRuVX0NXwoWW50KLxd2yw4Y1t2JUR5In1itQnudZqRMT9DlAM3Q//9NCjCFw==
dependencies:
errno "~0.1.1"
level-iterator-stream@~4.0.0:
version "4.0.2"
resolved "https://registry.yarnpkg.com/level-iterator-stream/-/level-iterator-stream-4.0.2.tgz#7ceba69b713b0d7e22fcc0d1f128ccdc8a24f79c"
integrity sha512-ZSthfEqzGSOMWoUGhTXdX9jv26d32XJuHz/5YnuHZzH6wldfWMOVwI9TBtKcya4BKTyTt3XVA0A3cF3q5CY30Q==
dependencies:
inherits "^2.0.4"
readable-stream "^3.4.0"
xtend "^4.0.2"
level-supports@~1.0.0:
version "1.0.1"
resolved "https://registry.yarnpkg.com/level-supports/-/level-supports-1.0.1.tgz#2f530a596834c7301622521988e2c36bb77d122d"
integrity sha512-rXM7GYnW8gsl1vedTJIbzOrRv85c/2uCMpiiCzO2fndd06U/kUXEEU9evYn4zFggBOg36IsBW8LzqIpETwwQzg==
dependencies:
xtend "^4.0.2"
levelup@^4.0.0:
version "4.3.2"
resolved "https://registry.yarnpkg.com/levelup/-/levelup-4.3.2.tgz#31c5b1b29f146d1d35d692e01a6da4d28fa55ebd"
integrity sha512-cRTjU4ktWo59wf13PHEiOayHC3n0dOh4i5+FHr4tv4MX9+l7mqETicNq3Aj07HKlLdk0z5muVoDL2RD+ovgiyA==
dependencies:
deferred-leveldown "~5.3.0"
level-errors "~2.0.0"
level-iterator-stream "~4.0.0"
level-supports "~1.0.0"
xtend "~4.0.0"
levn@^0.3.0, levn@~0.3.0:
version "0.3.0"
resolved "https://registry.yarnpkg.com/levn/-/levn-0.3.0.tgz#3b09924edf9f083c0490fdd4c0bc4421e04764ee"
@@ -5180,11 +5031,6 @@ log4js@^6.2.1:
rfdc "^1.1.4"
streamroller "^2.2.4"
long@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/long/-/long-4.0.0.tgz#9a7b71cfb7d361a194ea555241c92f7468d5bf28"
integrity sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA==
loud-rejection@^1.0.0:
version "1.6.0"
resolved "https://registry.yarnpkg.com/loud-rejection/-/loud-rejection-1.6.0.tgz#5b46f80147edee578870f086d04821cf998e551f"
@@ -6447,11 +6293,6 @@ quick-lru@^1.0.0:
resolved "https://registry.yarnpkg.com/quick-lru/-/quick-lru-1.1.0.tgz#4360b17c61136ad38078397ff11416e186dcfbb8"
integrity sha1-Q2CxfGETatOAeDl/8RQW4Ybc+7g=
random-js@^1.0.8:
version "1.0.8"
resolved "https://registry.yarnpkg.com/random-js/-/random-js-1.0.8.tgz#968fd689a6f25d6c0aac766283de2f688c9c190a"
integrity sha1-lo/WiabyXWwKrHZig94vaIycGQo=
randombytes@^2.0.0, randombytes@^2.0.1, randombytes@^2.0.5:
version "2.1.0"
resolved "https://registry.yarnpkg.com/randombytes/-/randombytes-2.1.0.tgz#df6f84372f0270dc65cdf6291349ab7a473d4f2a"
@@ -6591,7 +6432,7 @@ read@1, read@~1.0.1:
string_decoder "~1.1.1"
util-deprecate "~1.0.1"
"readable-stream@2 || 3", readable-stream@^3.0.2, readable-stream@^3.4.0:
"readable-stream@2 || 3", readable-stream@^3.0.2:
version "3.6.0"
resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.0.tgz#337bbda3adc0706bd3e024426a286d4b4b2c9198"
integrity sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==
@@ -8155,14 +7996,14 @@ xmlhttprequest-ssl@~1.5.4:
resolved "https://registry.yarnpkg.com/xmlhttprequest-ssl/-/xmlhttprequest-ssl-1.5.5.tgz#c2876b06168aadc40e57d97e81191ac8f4398b3e"
integrity sha1-wodrBhaKrcQOV9l+gRkayPQ5iz4=
xstream@^11.10.0, xstream@^11.11.0:
xstream@^11.11.0:
version "11.11.0"
resolved "https://registry.yarnpkg.com/xstream/-/xstream-11.11.0.tgz#2c963cf0a6cb3eafecb57eaeae16c9850235b422"
integrity sha512-0zF3PLsHrmbToPBgX1jYZFNw+t5octSFJgVRH44HGlGBBjY7gscvDQOZvty8IQV15Snia1RcLPECWDfEaE4aXg==
dependencies:
symbol-observable "1.2.0"
xtend@^4.0.0, xtend@^4.0.2, xtend@~4.0.0, xtend@~4.0.1:
xtend@^4.0.0, xtend@~4.0.1:
version "4.0.2"
resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.2.tgz#bb72779f5fa465186b1f438f674fa347fdb5db54"
integrity sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==