mirror of
https://github.com/cosmos/cosmjs.git
synced 2025-03-10 21:49:15 +00:00
Remove BCP package
This commit is contained in:
parent
dd120779aa
commit
fa5c62a797
@ -1 +0,0 @@
|
||||
../../.eslintignore
|
3
packages/bcp/.gitignore
vendored
3
packages/bcp/.gitignore
vendored
@ -1,3 +0,0 @@
|
||||
build/
|
||||
dist/
|
||||
docs/
|
@ -1,12 +0,0 @@
|
||||
# @iov/cosmos-sdk
|
||||
|
||||
[](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)).
|
@ -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();
|
@ -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,
|
||||
});
|
||||
};
|
@ -1 +0,0 @@
|
||||
Directory used to trigger lerna package updates for all packages
|
@ -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"
|
||||
}
|
||||
}
|
@ -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");
|
||||
});
|
||||
});
|
||||
});
|
@ -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;
|
||||
}
|
@ -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,
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
@ -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;
|
||||
}
|
||||
}
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
@ -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);
|
||||
}
|
||||
}
|
@ -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,
|
||||
};
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
*/
|
@ -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,
|
||||
};
|
||||
}
|
@ -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,
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
@ -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),
|
||||
},
|
||||
};
|
||||
}
|
@ -1,4 +0,0 @@
|
||||
export { CosmosCodec } from "./cosmoscodec";
|
||||
export { CosmosConnection, TokenConfiguration } from "./cosmosconnection";
|
||||
export { createCosmosConnector } from "./cosmosconnector";
|
||||
export { BankToken } from "./types";
|
@ -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
packages/bcp/src/testdata/cosmoshub.json
vendored
44
packages/bcp/src/testdata/cosmoshub.json
vendored
@ -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"
|
||||
}
|
@ -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();
|
||||
});
|
||||
});
|
@ -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);
|
||||
}
|
@ -1,12 +0,0 @@
|
||||
{
|
||||
"extends": "../../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"baseUrl": ".",
|
||||
"outDir": "build",
|
||||
"declarationDir": "build/types",
|
||||
"rootDir": "src"
|
||||
},
|
||||
"include": [
|
||||
"src/**/*"
|
||||
]
|
||||
}
|
@ -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
packages/bcp/types/address.d.ts
vendored
2
packages/bcp/types/address.d.ts
vendored
@ -1,2 +0,0 @@
|
||||
import { Address, PubkeyBundle } from "@iov/bcp";
|
||||
export declare function pubkeyToAddress(pubkey: PubkeyBundle, prefix: string): Address;
|
13
packages/bcp/types/caip5.d.ts
vendored
13
packages/bcp/types/caip5.d.ts
vendored
@ -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
packages/bcp/types/cosmoscodec.d.ts
vendored
24
packages/bcp/types/cosmoscodec.d.ts
vendored
@ -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
packages/bcp/types/cosmosconnection.d.ts
vendored
78
packages/bcp/types/cosmosconnection.d.ts
vendored
@ -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
packages/bcp/types/cosmosconnector.d.ts
vendored
11
packages/bcp/types/cosmosconnector.d.ts
vendored
@ -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
packages/bcp/types/decode.d.ts
vendored
52
packages/bcp/types/decode.d.ts
vendored
@ -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
packages/bcp/types/encode.d.ts
vendored
12
packages/bcp/types/encode.d.ts
vendored
@ -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
packages/bcp/types/index.d.ts
vendored
4
packages/bcp/types/index.d.ts
vendored
@ -1,4 +0,0 @@
|
||||
export { CosmosCodec } from "./cosmoscodec";
|
||||
export { CosmosConnection, TokenConfiguration } from "./cosmosconnection";
|
||||
export { createCosmosConnector } from "./cosmosconnector";
|
||||
export { BankToken } from "./types";
|
18
packages/bcp/types/types.d.ts
vendored
18
packages/bcp/types/types.d.ts
vendored
@ -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;
|
@ -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"])],
|
||||
},
|
||||
];
|
169
yarn.lock
169
yarn.lock
@ -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==
|
||||
|
Loading…
x
Reference in New Issue
Block a user