diff --git a/packages/bcp/package.json b/packages/bcp/package.json index ae98612d..1cc926a9 100644 --- a/packages/bcp/package.json +++ b/packages/bcp/package.json @@ -43,7 +43,6 @@ "@iov/crypto": "^2.0.0-alpha.7", "@iov/encoding": "^2.0.0-alpha.7", "@iov/stream": "^2.0.0-alpha.7", - "@tendermint/amino-js": "^0.7.0-alpha.1", "fast-deep-equal": "^3.1.1", "readonly-date": "^1.0.0", "xstream": "^11.11.0" diff --git a/packages/bcp/src/address.spec.ts b/packages/bcp/src/address.spec.ts index 1d138206..21fcb192 100644 --- a/packages/bcp/src/address.spec.ts +++ b/packages/bcp/src/address.spec.ts @@ -33,8 +33,8 @@ describe("address", () => { expect( decodeCosmosPubkey("cosmospub1addwnpepqd8sgxq7aw348ydctp3n5ajufgxp395hksxjzc6565yfp56scupfqhlgyg5"), ).toEqual({ - prefix: "cosmospub", data: fromBase64("A08EGB7ro1ORuFhjOnZcSgwYlpe0DSFjVNUIkNNQxwKQ"), + algo: Algorithm.Secp256k1, }); }); }); diff --git a/packages/bcp/src/address.ts b/packages/bcp/src/address.ts index d36a50d4..fa5e7ff2 100644 --- a/packages/bcp/src/address.ts +++ b/packages/bcp/src/address.ts @@ -1,4 +1,4 @@ -import { Address, Algorithm, PubkeyBundle } from "@iov/bcp"; +import { Address, Algorithm, PubkeyBundle, PubkeyBytes } from "@iov/bcp"; import { Ripemd160, Secp256k1, Sha256 } from "@iov/crypto"; import { Bech32, Encoding } from "@iov/encoding"; import equal from "fast-deep-equal"; @@ -8,7 +8,10 @@ export type CosmosPubkeyBech32Prefix = "cosmospub" | "cosmosvalconspub" | "cosmo export type CosmosBech32Prefix = CosmosAddressBech32Prefix | CosmosPubkeyBech32Prefix; // As discussed in https://github.com/binance-chain/javascript-sdk/issues/163 -const pubkeyAminoPrefix = Encoding.fromHex("eb5ae98721"); +// Prefixes listed here: https://github.com/tendermint/tendermint/blob/d419fffe18531317c28c29a292ad7d253f6cafdf/docs/spec/blockchain/encoding.md#public-key-cryptography +const pubkeyAminoPrefixSecp256k1 = Encoding.fromHex("eb5ae98721"); +const pubkeyAminoPrefixEd25519 = Encoding.fromHex("1624de64"); +const pubkeyAminoPrefixLength = pubkeyAminoPrefixSecp256k1.length; function isCosmosAddressBech32Prefix(prefix: string): prefix is CosmosAddressBech32Prefix { return ["cosmos", "cosmosvalcons", "cosmosvaloper"].includes(prefix); @@ -33,22 +36,27 @@ export function decodeCosmosAddress( export function decodeCosmosPubkey( encodedPubkey: string, -): { readonly prefix: CosmosPubkeyBech32Prefix; readonly data: Uint8Array } { +): { readonly algo: Algorithm; readonly data: PubkeyBytes } { const { prefix, data } = Bech32.decode(encodedPubkey); if (!isCosmosPubkeyBech32Prefix(prefix)) { throw new Error(`Invalid bech32 prefix. Must be one of cosmos, cosmosvalcons, or cosmosvaloper.`); } - if (!equal(data.slice(0, pubkeyAminoPrefix.length), pubkeyAminoPrefix)) { - throw new Error("Pubkey does not have the expected amino prefix " + Encoding.toHex(pubkeyAminoPrefix)); + const aminoPrefix = data.slice(0, pubkeyAminoPrefixLength); + const rest = data.slice(pubkeyAminoPrefixLength); + if (equal(aminoPrefix, pubkeyAminoPrefixSecp256k1)) { + if (rest.length !== 33) { + throw new Error("Invalid rest data length. Expected 33 bytes (compressed secp256k1 pubkey)."); + } + return { algo: Algorithm.Secp256k1, data: rest as PubkeyBytes }; + } else if (equal(aminoPrefix, pubkeyAminoPrefixEd25519)) { + if (rest.length !== 32) { + throw new Error("Invalid rest data length. Expected 32 bytes (ed25519 pubkey)."); + } + return { algo: Algorithm.Ed25519, data: rest as PubkeyBytes }; + } else { + throw new Error("Unsupported Pubkey type. Amino prefix: " + Encoding.toHex(aminoPrefix)); } - - const rest = data.slice(pubkeyAminoPrefix.length); - if (rest.length !== 33) { - throw new Error("Invalid rest data length. Expected 33 bytes (compressed secp256k1 pubkey)."); - } - - return { prefix: prefix, data: rest }; } export function isValidAddress(address: string): boolean { diff --git a/packages/bcp/src/cosmwasmcodec.spec.ts b/packages/bcp/src/cosmwasmcodec.spec.ts index 1829b936..8f5604be 100644 --- a/packages/bcp/src/cosmwasmcodec.spec.ts +++ b/packages/bcp/src/cosmwasmcodec.spec.ts @@ -2,7 +2,7 @@ import { PostableBytes, PrehashType } from "@iov/bcp"; import { Encoding } from "@iov/encoding"; import { cosmWasmCodec } from "./cosmwasmcodec"; -import { chainId, nonce, sendTxJson, signedTxBin, signedTxJson, txId } from "./testdata.spec"; +import { chainId, nonce, sendTxJson, signedTxBin, signedTxEncodedJson, signedTxJson } from "./testdata.spec"; const { toUtf8 } = Encoding; @@ -21,7 +21,7 @@ describe("cosmWasmCodec", () => { it("properly encodes transactions", () => { const encoded = cosmWasmCodec.bytesToPost(signedTxJson); - expect(encoded).toEqual(signedTxBin); + expect(encoded).toEqual(signedTxEncodedJson); }); it("throws when trying to decode a transaction without a nonce", () => { @@ -31,16 +31,10 @@ describe("cosmWasmCodec", () => { }); it("properly decodes transactions", () => { - const decoded = cosmWasmCodec.parseBytes(signedTxBin as PostableBytes, chainId, nonce); + const decoded = cosmWasmCodec.parseBytes(signedTxEncodedJson as PostableBytes, chainId, nonce); expect(decoded).toEqual(signedTxJson); }); - it("generates transaction id", () => { - const id = cosmWasmCodec.identifier(signedTxJson); - expect(id).toMatch(/^[0-9A-F]{64}$/); - expect(id).toEqual(txId); - }); - it("round trip works", () => { const encoded = cosmWasmCodec.bytesToPost(signedTxJson); const decoded = cosmWasmCodec.parseBytes(encoded, chainId, nonce); diff --git a/packages/bcp/src/cosmwasmcodec.ts b/packages/bcp/src/cosmwasmcodec.ts index 30650ae0..2ca6e273 100644 --- a/packages/bcp/src/cosmwasmcodec.ts +++ b/packages/bcp/src/cosmwasmcodec.ts @@ -1,4 +1,5 @@ /* eslint-disable @typescript-eslint/camelcase */ +import { marshalTx, unmarshalTx } from "@cosmwasm/sdk"; import { Address, ChainId, @@ -13,9 +14,7 @@ import { TxCodec, UnsignedTransaction, } from "@iov/bcp"; -import { Sha256 } from "@iov/crypto"; import { Encoding } from "@iov/encoding"; -import { marshalTx, unmarshalTx } from "@tendermint/amino-js"; import { CosmosBech32Prefix, isValidAddress, pubkeyToAddress } from "./address"; import { Caip5 } from "./caip5"; @@ -23,7 +22,7 @@ import { parseTx } from "./decode"; import { buildSignedTx, buildUnsignedTx } from "./encode"; import { nonceToAccountNumber, nonceToSequence, TokenInfos } from "./types"; -const { toHex, toUtf8 } = Encoding; +const { toUtf8 } = Encoding; function sortJson(json: any): any { if (typeof json !== "object" || json === null) { @@ -72,16 +71,20 @@ export class CosmWasmCodec implements TxCodec { }; } + // PostableBytes are JSON-encoded StdTx public bytesToPost(signed: SignedTransaction): PostableBytes { + // TODO: change this as well (return StdTx, not AminoTx)? const built = buildSignedTx(signed, this.tokens); - const bytes = marshalTx(built, true); - return bytes as PostableBytes; + return marshalTx(built.value) as PostableBytes; } - public identifier(signed: SignedTransaction): TransactionId { - const bytes = this.bytesToPost(signed); - const hash = new Sha256(bytes).digest(); - return toHex(hash).toUpperCase() as TransactionId; + // 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 { diff --git a/packages/bcp/src/cosmwasmconnection.spec.ts b/packages/bcp/src/cosmwasmconnection.spec.ts index 8ef2a08e..100ddc3b 100644 --- a/packages/bcp/src/cosmwasmconnection.spec.ts +++ b/packages/bcp/src/cosmwasmconnection.spec.ts @@ -17,6 +17,7 @@ import { HdPaths, Secp256k1HdWallet, UserProfile } from "@iov/keycontrol"; import { CosmosBech32Prefix } from "./address"; import { CosmWasmCodec, cosmWasmCodec } from "./cosmwasmcodec"; import { CosmWasmConnection, TokenConfiguration } from "./cosmwasmconnection"; +import { signedTxJson, txId } from "./testdata.spec"; import { nonceToSequence } from "./types"; const { fromBase64, toHex } = Encoding; @@ -133,6 +134,17 @@ describe("CosmWasmConnection", () => { }); }); + describe("identifier", () => { + it("calculates tx hash from PostableBytes", async () => { + pendingWithoutCosmos(); + const connection = await CosmWasmConnection.establish(httpUrl, defaultPrefix, defaultTokens); + const postable = cosmWasmCodec.bytesToPost(signedTxJson); + const id = await connection.identifier(postable); + expect(id).toMatch(/^[0-9A-F]{64}$/); + expect(id).toEqual(txId); + }); + }); + describe("getAccount", () => { it("gets an empty account by address", async () => { pendingWithoutCosmos(); diff --git a/packages/bcp/src/cosmwasmconnection.ts b/packages/bcp/src/cosmwasmconnection.ts index 53779038..84177c3a 100644 --- a/packages/bcp/src/cosmwasmconnection.ts +++ b/packages/bcp/src/cosmwasmconnection.ts @@ -1,10 +1,9 @@ /* eslint-disable @typescript-eslint/camelcase */ -import { RestClient, TxsResponse } from "@cosmwasm/sdk"; +import { RestClient, TxsResponse, unmarshalTx } from "@cosmwasm/sdk"; import { Account, AccountQuery, AddressQuery, - Algorithm, BlockchainConnection, BlockHeader, BlockId, @@ -20,7 +19,6 @@ import { Nonce, PostableBytes, PostTxResponse, - PubkeyBytes, PubkeyQuery, Token, TokenTicker, @@ -29,7 +27,8 @@ import { TransactionState, UnsignedTransaction, } from "@iov/bcp"; -import { Uint53 } from "@iov/encoding"; +import { Sha256 } from "@iov/crypto"; +import { Encoding, Uint53 } from "@iov/encoding"; import { DefaultValueProducer, ValueAndUpdates } from "@iov/stream"; import equal from "fast-deep-equal"; import { ReadonlyDate } from "readonly-date"; @@ -40,6 +39,8 @@ import { Caip5 } from "./caip5"; import { decodeAmount, parseTxsResponse } from "./decode"; import { accountToNonce, TokenInfo } from "./types"; +const { toHex } = Encoding; + interface ChainData { readonly chainId: ChainId; } @@ -68,7 +69,7 @@ function buildQueryString({ return components.filter(Boolean).join("&"); } -export type TokenConfiguration = readonly (TokenInfo & { readonly name: string })[]; +export type TokenConfiguration = ReadonlyArray; export class CosmWasmConnection implements BlockchainConnection { // we must know prefix and tokens a priori to understand the chain @@ -140,6 +141,13 @@ export class CosmWasmConnection implements BlockchainConnection { return this.supportedTokens; } + public async identifier(signed: PostableBytes): Promise { + const tx = unmarshalTx(signed); + const bytes = await this.restClient.encodeTx(tx); + const hash = new Sha256(bytes).digest(); + return toHex(hash).toUpperCase() as TransactionId; + } + public async getAccount(query: AccountQuery): Promise { const address = isPubkeyQuery(query) ? pubkeyToAddress(query.pubkey, this.prefix) : query.address; const { result } = await this.restClient.authAccounts(address); @@ -151,13 +159,7 @@ export class CosmWasmConnection implements BlockchainConnection { this.tokenInfo.find(token => token.denom === denom), ); - const pubkey = !account.public_key - ? undefined - : { - algo: Algorithm.Secp256k1, - // amino-js has wrong (outdated) types - data: decodeCosmosPubkey(account.public_key as any).data as PubkeyBytes, - }; + const pubkey = !account.public_key ? undefined : decodeCosmosPubkey(account.public_key); return { address: address, balance: supportedCoins.map(coin => decodeAmount(this.tokenInfo, coin)), diff --git a/packages/bcp/src/decode.spec.ts b/packages/bcp/src/decode.spec.ts index 3c825555..c783b5a4 100644 --- a/packages/bcp/src/decode.spec.ts +++ b/packages/bcp/src/decode.spec.ts @@ -1,7 +1,7 @@ /* eslint-disable @typescript-eslint/camelcase */ +import { types } from "@cosmwasm/sdk"; import { Address, Algorithm, TokenTicker } from "@iov/bcp"; import { Encoding } from "@iov/encoding"; -import amino from "@tendermint/amino-js"; import { decodeAmount, @@ -113,7 +113,7 @@ describe("decode", () => { describe("decodeAmount", () => { it("works", () => { - const amount: amino.Coin = { + const amount: types.Coin = { denom: "uatom", amount: "11657995", }; @@ -123,7 +123,7 @@ describe("decode", () => { describe("parseMsg", () => { it("works", () => { - const msg: amino.Msg = { + const msg: types.Msg = { type: "cosmos-sdk/MsgSend", value: { from_address: "cosmos1h806c7khnvmjlywdrkdgk2vrayy2mmvf9rxk2r", @@ -157,7 +157,7 @@ describe("decode", () => { describe("parseTx", () => { it("works", () => { - expect(parseTx(data.tx, chainId, nonce, defaultTokens)).toEqual(signedTxJson); + expect(parseTx(data.tx.value, chainId, nonce, defaultTokens)).toEqual(signedTxJson); }); }); @@ -181,3 +181,179 @@ describe("decode", () => { }); }); }); + +/* + +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 + } +} + + */ diff --git a/packages/bcp/src/decode.ts b/packages/bcp/src/decode.ts index 1343dc89..2835b0fa 100644 --- a/packages/bcp/src/decode.ts +++ b/packages/bcp/src/decode.ts @@ -1,4 +1,4 @@ -import { isAminoStdTx, TxsResponse } from "@cosmwasm/sdk"; +import { TxsResponse, types } from "@cosmwasm/sdk"; import { Address, Algorithm, @@ -18,13 +18,12 @@ import { UnsignedTransaction, } from "@iov/bcp"; import { Decimal, Encoding } from "@iov/encoding"; -import amino from "@tendermint/amino-js"; import { TokenInfos } from "./types"; const { fromBase64 } = Encoding; -export function decodePubkey(pubkey: amino.PubKey): PubkeyBundle { +export function decodePubkey(pubkey: types.PubKey): PubkeyBundle { switch (pubkey.type) { // https://github.com/tendermint/tendermint/blob/v0.33.0/crypto/secp256k1/secp256k1.go#L23 case "tendermint/PubKeySecp256k1": @@ -47,7 +46,7 @@ export function decodeSignature(signature: string): SignatureBytes { return fromBase64(signature) as SignatureBytes; } -export function decodeFullSignature(signature: amino.StdSignature, nonce: number): FullSignature { +export function decodeFullSignature(signature: types.StdSignature, nonce: number): FullSignature { return { nonce: nonce as Nonce, pubkey: decodePubkey(signature.pub_key), @@ -55,7 +54,7 @@ export function decodeFullSignature(signature: amino.StdSignature, nonce: number }; } -export function coinToDecimal(tokens: TokenInfos, coin: amino.Coin): readonly [Decimal, string] { +export function coinToDecimal(tokens: TokenInfos, coin: types.Coin): readonly [Decimal, string] { const match = tokens.find(({ denom }) => denom === coin.denom); if (!match) { throw Error(`unknown denom: ${coin.denom}`); @@ -64,7 +63,7 @@ export function coinToDecimal(tokens: TokenInfos, coin: amino.Coin): readonly [D return [value, match.ticker]; } -export function decodeAmount(tokens: TokenInfos, coin: amino.Coin): Amount { +export function decodeAmount(tokens: TokenInfos, coin: types.Coin): Amount { const [value, ticker] = coinToDecimal(tokens, coin); return { quantity: value.atomics, @@ -73,14 +72,14 @@ export function decodeAmount(tokens: TokenInfos, coin: amino.Coin): Amount { }; } -export function parseMsg(msg: amino.Msg, chainId: ChainId, tokens: TokenInfos): SendTransaction { +export function parseMsg(msg: types.Msg, chainId: ChainId, tokens: TokenInfos): SendTransaction { if (msg.type !== "cosmos-sdk/MsgSend") { throw new Error("Unknown message type in transaction"); } - if (!(msg.value as amino.MsgSend).from_address) { + if (!(msg.value as types.MsgSend).from_address) { throw new Error("Only MsgSend is supported"); } - const msgValue = msg.value as amino.MsgSend; + const msgValue = msg.value as types.MsgSend; if (msgValue.amount.length !== 1) { throw new Error("Only MsgSend with one amount is supported"); } @@ -93,7 +92,7 @@ export function parseMsg(msg: amino.Msg, chainId: ChainId, tokens: TokenInfos): }; } -export function parseFee(fee: amino.StdFee, tokens: TokenInfos): Fee { +export function parseFee(fee: types.StdFee, tokens: TokenInfos): Fee { if (fee.amount.length !== 1) { throw new Error("Only fee with one amount is supported"); } @@ -103,9 +102,13 @@ export function parseFee(fee: amino.StdFee, tokens: TokenInfos): Fee { }; } -export function parseTx(tx: amino.Tx, chainId: ChainId, nonce: Nonce, tokens: TokenInfos): SignedTransaction { - const txValue = tx.value; - if (!isAminoStdTx(txValue)) { +export function parseTx( + txValue: types.StdTx, + chainId: ChainId, + nonce: Nonce, + tokens: TokenInfos, +): SignedTransaction { + if (!types.isAminoStdTx(txValue)) { throw new Error("Only Amino StdTx is supported"); } if (txValue.msg.length !== 1) { @@ -138,7 +141,7 @@ export function parseTxsResponse( ): ConfirmedAndSignedTransaction { const height = parseInt(response.height, 10); return { - ...parseTx(response.tx, chainId, nonce, tokens), + ...parseTx(response.tx.value, chainId, nonce, tokens), height: height, confirmations: currentHeight - height + 1, transactionId: response.txhash as TransactionId, diff --git a/packages/bcp/src/encode.ts b/packages/bcp/src/encode.ts index 5c6bc7e1..94288c48 100644 --- a/packages/bcp/src/encode.ts +++ b/packages/bcp/src/encode.ts @@ -1,5 +1,5 @@ /* eslint-disable @typescript-eslint/camelcase */ -import { AminoTx } from "@cosmwasm/sdk"; +import { types } from "@cosmwasm/sdk"; import { Algorithm, Amount, @@ -12,13 +12,12 @@ import { } from "@iov/bcp"; import { Secp256k1 } from "@iov/crypto"; import { Decimal, Encoding } from "@iov/encoding"; -import amino from "@tendermint/amino-js"; import { TokenInfos } from "./types"; const { toBase64 } = Encoding; -export function encodePubkey(pubkey: PubkeyBundle): amino.PubKey { +export function encodePubkey(pubkey: PubkeyBundle): types.PubKey { switch (pubkey.algo) { case Algorithm.Secp256k1: return { @@ -35,7 +34,7 @@ export function encodePubkey(pubkey: PubkeyBundle): amino.PubKey { } } -export function decimalToCoin(lookup: TokenInfos, value: Decimal, ticker: string): amino.Coin { +export function decimalToCoin(lookup: TokenInfos, value: Decimal, ticker: string): types.Coin { const match = lookup.find(token => token.ticker === ticker); if (!match) { throw Error(`unknown ticker: ${ticker}`); @@ -51,7 +50,7 @@ export function decimalToCoin(lookup: TokenInfos, value: Decimal, ticker: string }; } -export function encodeAmount(amount: Amount, tokens: TokenInfos): amino.Coin { +export function encodeAmount(amount: Amount, tokens: TokenInfos): types.Coin { return decimalToCoin( tokens, Decimal.fromAtomics(amount.quantity, amount.fractionalDigits), @@ -59,7 +58,7 @@ export function encodeAmount(amount: Amount, tokens: TokenInfos): amino.Coin { ); } -export function encodeFee(fee: Fee, tokens: TokenInfos): amino.StdFee { +export function encodeFee(fee: Fee, tokens: TokenInfos): types.StdFee { if (fee.tokens === undefined) { throw new Error("Cannot encode fee without tokens"); } @@ -72,7 +71,7 @@ export function encodeFee(fee: Fee, tokens: TokenInfos): amino.StdFee { }; } -export function encodeFullSignature(fullSignature: FullSignature): amino.StdSignature { +export function encodeFullSignature(fullSignature: FullSignature): types.StdSignature { return { pub_key: { type: "tendermint/PubKeySecp256k1", @@ -83,7 +82,7 @@ export function encodeFullSignature(fullSignature: FullSignature): amino.StdSign }; } -export function buildUnsignedTx(tx: UnsignedTransaction, tokens: TokenInfos): AminoTx { +export function buildUnsignedTx(tx: UnsignedTransaction, tokens: TokenInfos): types.AminoTx { if (!isSendTransaction(tx)) { throw new Error("Received transaction of unsupported kind"); } @@ -112,7 +111,7 @@ export function buildUnsignedTx(tx: UnsignedTransaction, tokens: TokenInfos): Am }; } -export function buildSignedTx(tx: SignedTransaction, tokens: TokenInfos): AminoTx { +export function buildSignedTx(tx: SignedTransaction, tokens: TokenInfos): types.AminoTx { const built = buildUnsignedTx(tx.transaction, tokens); return { ...built, diff --git a/packages/bcp/src/testdata.spec.ts b/packages/bcp/src/testdata.spec.ts index bf627e38..bec40b5b 100644 --- a/packages/bcp/src/testdata.spec.ts +++ b/packages/bcp/src/testdata.spec.ts @@ -16,7 +16,7 @@ import { Encoding } from "@iov/encoding"; import data from "./testdata/cosmoshub.json"; -const { fromBase64 } = Encoding; +const { fromBase64, toUtf8 } = Encoding; export const pubJson: PubkeyBundle = { algo: Algorithm.Secp256k1, @@ -62,3 +62,7 @@ export const signedTxJson: SignedTransaction = { 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"}}`, +); diff --git a/packages/bcp/types/address.d.ts b/packages/bcp/types/address.d.ts index 252add6e..fd3a99c3 100644 --- a/packages/bcp/types/address.d.ts +++ b/packages/bcp/types/address.d.ts @@ -1,4 +1,4 @@ -import { Address, PubkeyBundle } from "@iov/bcp"; +import { Address, Algorithm, PubkeyBundle, PubkeyBytes } from "@iov/bcp"; export declare type CosmosAddressBech32Prefix = "cosmos" | "cosmosvalcons" | "cosmosvaloper"; export declare type CosmosPubkeyBech32Prefix = "cosmospub" | "cosmosvalconspub" | "cosmosvaloperpub"; export declare type CosmosBech32Prefix = CosmosAddressBech32Prefix | CosmosPubkeyBech32Prefix; @@ -11,8 +11,8 @@ export declare function decodeCosmosAddress( export declare function decodeCosmosPubkey( encodedPubkey: string, ): { - readonly prefix: CosmosPubkeyBech32Prefix; - readonly data: Uint8Array; + readonly algo: Algorithm; + readonly data: PubkeyBytes; }; export declare function isValidAddress(address: string): boolean; export declare function pubkeyToAddress(pubkey: PubkeyBundle, prefix: CosmosBech32Prefix): Address; diff --git a/packages/bcp/types/cosmwasmcodec.d.ts b/packages/bcp/types/cosmwasmcodec.d.ts index 1373ef99..9ce8a6f0 100644 --- a/packages/bcp/types/cosmwasmcodec.d.ts +++ b/packages/bcp/types/cosmwasmcodec.d.ts @@ -18,7 +18,7 @@ export declare class CosmWasmCodec implements TxCodec { constructor(prefix: CosmosBech32Prefix, tokens: TokenInfos); bytesToSign(unsigned: UnsignedTransaction, nonce: Nonce): SigningJob; bytesToPost(signed: SignedTransaction): PostableBytes; - identifier(signed: SignedTransaction): TransactionId; + identifier(_signed: SignedTransaction): TransactionId; parseBytes(bytes: PostableBytes, chainId: ChainId, nonce?: Nonce): SignedTransaction; identityToAddress(identity: Identity): Address; isValidAddress(address: string): boolean; diff --git a/packages/bcp/types/cosmwasmconnection.d.ts b/packages/bcp/types/cosmwasmconnection.d.ts index 764e9512..e5d1a7c6 100644 --- a/packages/bcp/types/cosmwasmconnection.d.ts +++ b/packages/bcp/types/cosmwasmconnection.d.ts @@ -22,9 +22,11 @@ import { import { Stream } from "xstream"; import { CosmosBech32Prefix } from "./address"; import { TokenInfo } from "./types"; -export declare type TokenConfiguration = readonly (TokenInfo & { - readonly name: string; -})[]; +export declare type TokenConfiguration = ReadonlyArray< + TokenInfo & { + readonly name: string; + } +>; export declare class CosmWasmConnection implements BlockchainConnection { static establish( url: string, @@ -45,6 +47,7 @@ export declare class CosmWasmConnection implements BlockchainConnection { height(): Promise; getToken(searchTicker: TokenTicker): Promise; getAllTokens(): Promise; + identifier(signed: PostableBytes): Promise; getAccount(query: AccountQuery): Promise; watchAccount(_account: AccountQuery): Stream; getNonce(query: AddressQuery | PubkeyQuery): Promise; diff --git a/packages/bcp/types/decode.d.ts b/packages/bcp/types/decode.d.ts index 8a30ebbb..2fb8dcab 100644 --- a/packages/bcp/types/decode.d.ts +++ b/packages/bcp/types/decode.d.ts @@ -1,4 +1,4 @@ -import { TxsResponse } from "@cosmwasm/sdk"; +import { TxsResponse, types } from "@cosmwasm/sdk"; import { Amount, ChainId, @@ -13,17 +13,16 @@ import { UnsignedTransaction, } from "@iov/bcp"; import { Decimal } from "@iov/encoding"; -import amino from "@tendermint/amino-js"; import { TokenInfos } from "./types"; -export declare function decodePubkey(pubkey: amino.PubKey): PubkeyBundle; +export declare function decodePubkey(pubkey: types.PubKey): PubkeyBundle; export declare function decodeSignature(signature: string): SignatureBytes; -export declare function decodeFullSignature(signature: amino.StdSignature, nonce: number): FullSignature; -export declare function coinToDecimal(tokens: TokenInfos, coin: amino.Coin): readonly [Decimal, string]; -export declare function decodeAmount(tokens: TokenInfos, coin: amino.Coin): Amount; -export declare function parseMsg(msg: amino.Msg, chainId: ChainId, tokens: TokenInfos): SendTransaction; -export declare function parseFee(fee: amino.StdFee, tokens: TokenInfos): Fee; +export declare function decodeFullSignature(signature: types.StdSignature, nonce: number): FullSignature; +export declare function coinToDecimal(tokens: TokenInfos, coin: types.Coin): readonly [Decimal, string]; +export declare function decodeAmount(tokens: TokenInfos, coin: types.Coin): Amount; +export declare function parseMsg(msg: types.Msg, chainId: ChainId, tokens: TokenInfos): SendTransaction; +export declare function parseFee(fee: types.StdFee, tokens: TokenInfos): Fee; export declare function parseTx( - tx: amino.Tx, + txValue: types.StdTx, chainId: ChainId, nonce: Nonce, tokens: TokenInfos, diff --git a/packages/bcp/types/encode.d.ts b/packages/bcp/types/encode.d.ts index dddaf1d2..7af2c690 100644 --- a/packages/bcp/types/encode.d.ts +++ b/packages/bcp/types/encode.d.ts @@ -1,12 +1,11 @@ -import { AminoTx } from "@cosmwasm/sdk"; +import { types } from "@cosmwasm/sdk"; import { Amount, Fee, FullSignature, PubkeyBundle, SignedTransaction, UnsignedTransaction } from "@iov/bcp"; import { Decimal } from "@iov/encoding"; -import amino from "@tendermint/amino-js"; import { TokenInfos } from "./types"; -export declare function encodePubkey(pubkey: PubkeyBundle): amino.PubKey; -export declare function decimalToCoin(lookup: TokenInfos, value: Decimal, ticker: string): amino.Coin; -export declare function encodeAmount(amount: Amount, tokens: TokenInfos): amino.Coin; -export declare function encodeFee(fee: Fee, tokens: TokenInfos): amino.StdFee; -export declare function encodeFullSignature(fullSignature: FullSignature): amino.StdSignature; -export declare function buildUnsignedTx(tx: UnsignedTransaction, tokens: TokenInfos): AminoTx; -export declare function buildSignedTx(tx: SignedTransaction, tokens: TokenInfos): AminoTx; +export declare function encodePubkey(pubkey: PubkeyBundle): types.PubKey; +export declare function decimalToCoin(lookup: TokenInfos, value: Decimal, ticker: string): types.Coin; +export declare function encodeAmount(amount: Amount, tokens: TokenInfos): types.Coin; +export declare function encodeFee(fee: Fee, tokens: TokenInfos): types.StdFee; +export declare function encodeFullSignature(fullSignature: FullSignature): types.StdSignature; +export declare function buildUnsignedTx(tx: UnsignedTransaction, tokens: TokenInfos): types.AminoTx; +export declare function buildSignedTx(tx: SignedTransaction, tokens: TokenInfos): types.AminoTx; diff --git a/packages/sdk/package.json b/packages/sdk/package.json index e3a7ccf6..ab71861e 100644 --- a/packages/sdk/package.json +++ b/packages/sdk/package.json @@ -39,7 +39,6 @@ }, "dependencies": { "@iov/encoding": "^2.0.0-alpha.7", - "@tendermint/amino-js": "^0.7.0-alpha.1", "axios": "^0.19.0" }, "devDependencies": { diff --git a/packages/sdk/src/decoding.ts b/packages/sdk/src/decoding.ts index e69de29b..11560d12 100644 --- a/packages/sdk/src/decoding.ts +++ b/packages/sdk/src/decoding.ts @@ -0,0 +1,11 @@ +import { Encoding } from "@iov/encoding"; + +import { isAminoStdTx, StdTx } from "./types"; + +export function unmarshalTx(data: Uint8Array): StdTx { + const decoded = JSON.parse(Encoding.fromUtf8(data)); + if (!isAminoStdTx(decoded)) { + throw new Error("Must be json encoded StdTx"); + } + return decoded; +} diff --git a/packages/sdk/src/encoding.ts b/packages/sdk/src/encoding.ts index e69de29b..6f6d9be3 100644 --- a/packages/sdk/src/encoding.ts +++ b/packages/sdk/src/encoding.ts @@ -0,0 +1,8 @@ +import { Encoding } from "@iov/encoding"; + +import { StdTx } from "./types"; + +export function marshalTx(tx: StdTx): Uint8Array { + const json = JSON.stringify(tx); + return Encoding.toUtf8(json); +} diff --git a/packages/sdk/src/index.ts b/packages/sdk/src/index.ts index f8dbdc95..d84968e5 100644 --- a/packages/sdk/src/index.ts +++ b/packages/sdk/src/index.ts @@ -1,2 +1,6 @@ +import * as types from "./types"; + +export { unmarshalTx } from "./decoding"; +export { marshalTx } from "./encoding"; export { RestClient, TxsResponse } from "./restclient"; -export { AminoTx, isAminoStdTx } from "./types"; +export { types }; diff --git a/packages/sdk/src/restclient.ts b/packages/sdk/src/restclient.ts index 3bd74f70..c7d17e94 100644 --- a/packages/sdk/src/restclient.ts +++ b/packages/sdk/src/restclient.ts @@ -1,7 +1,9 @@ -import amino, { unmarshalTx } from "@tendermint/amino-js"; +import { Encoding } from "@iov/encoding"; import axios, { AxiosInstance } from "axios"; -import { AminoTx } from "./types"; +import { AminoTx, BaseAccount, isAminoStdTx, StdTx } from "./types"; + +const { fromUtf8 } = Encoding; interface NodeInfo { readonly network: string; @@ -35,7 +37,7 @@ interface BlocksResponse { interface AuthAccountsResponse { readonly result: { - readonly value: amino.BaseAccount; + readonly value: BaseAccount; }; } @@ -64,13 +66,19 @@ interface PostTxsResponse { readonly raw_log?: string; } +interface EncodeTxResponse { + // base64-encoded amino-binary encoded representation + readonly tx: string; +} + type RestClientResponse = | NodeInfoResponse | BlocksResponse | AuthAccountsResponse | TxsResponse | SearchTxsResponse - | PostTxsResponse; + | PostTxsResponse + | EncodeTxResponse; type BroadcastMode = "block" | "sync" | "async"; @@ -131,6 +139,16 @@ export class RestClient { return responseData as BlocksResponse; } + // encodeTx returns the amino-encoding of the transaction + public async encodeTx(stdTx: StdTx): Promise { + const tx = { type: "cosmos-sdk/StdTx", value: stdTx }; + const responseData = await this.post("/txs/encode", tx); + if (!(responseData as any).tx) { + throw new Error("Unexpected response data format"); + } + return Encoding.fromBase64((responseData as EncodeTxResponse).tx); + } + public async authAccounts(address: string, height?: string): Promise { const path = height === undefined ? `/auth/accounts/${address}` : `/auth/accounts/${address}?tx.height=${height}`; @@ -157,10 +175,15 @@ export class RestClient { return responseData as TxsResponse; } + // tx must be JSON encoded StdTx (no wrapper) public async postTx(tx: Uint8Array): Promise { - const unmarshalled = unmarshalTx(tx, true); + // TODO: check this is StdTx + const decoded = JSON.parse(fromUtf8(tx)); + if (!isAminoStdTx(decoded)) { + throw new Error("Must be json encoded StdTx"); + } const params = { - tx: unmarshalled.value, + tx: decoded, mode: this.mode, }; const responseData = await this.post("/txs", params); diff --git a/packages/sdk/src/types.ts b/packages/sdk/src/types.ts index c8a0ef72..1d5e9c86 100644 --- a/packages/sdk/src/types.ts +++ b/packages/sdk/src/types.ts @@ -1,10 +1,69 @@ -import amino from "@tendermint/amino-js"; +// We will move all needed *interfaces* from amino-js here +// This means bcp can just import them from here (if needed at all) +export interface Tx { + readonly type: string; + // TODO + readonly value: unknown; +} -export type AminoTx = amino.Tx & { readonly value: amino.StdTx }; +export interface StdTx { + readonly msg: ReadonlyArray; + readonly fee: StdFee; + readonly signatures: ReadonlyArray; + readonly memo: string | undefined; +} -export function isAminoStdTx(txValue: amino.TxValue): txValue is amino.StdTx { - const { memo, msg, fee, signatures } = txValue as amino.StdTx; +export type AminoTx = Tx & { readonly value: StdTx }; + +export function isAminoStdTx(txValue: unknown): txValue is StdTx { + const { memo, msg, fee, signatures } = txValue as StdTx; return ( typeof memo === "string" && Array.isArray(msg) && typeof fee === "object" && Array.isArray(signatures) ); } + +export interface Msg { + readonly type: string; + // TODO: make better union type + readonly value: MsgSend | unknown; +} + +export interface MsgSend { + /** Bech32 account address */ + readonly from_address: string; + /** Bech32 account address */ + readonly to_address: string; + readonly amount: ReadonlyArray; +} + +export interface StdFee { + readonly amount: ReadonlyArray; + readonly gas: string; +} + +export interface Coin { + readonly denom: string; + readonly amount: string; +} + +export interface StdSignature { + readonly pub_key: PubKey; + readonly signature: string; +} + +export interface PubKey { + readonly type: string; + readonly value: string; +} + +// AccountPubKey is bech32-encoded amino-binary encoded PubKey interface. oof. +export type AccountPubKey = string; + +export interface BaseAccount { + /** Bech32 account address */ + readonly address: string; + readonly coins: ReadonlyArray; + readonly public_key: AccountPubKey; + readonly account_number: string; + readonly sequence: string; +} diff --git a/packages/sdk/types/decoding.d.ts b/packages/sdk/types/decoding.d.ts index e69de29b..fd0c5746 100644 --- a/packages/sdk/types/decoding.d.ts +++ b/packages/sdk/types/decoding.d.ts @@ -0,0 +1,2 @@ +import { StdTx } from "./types"; +export declare function unmarshalTx(data: Uint8Array): StdTx; diff --git a/packages/sdk/types/encoding.d.ts b/packages/sdk/types/encoding.d.ts index e69de29b..7a33940a 100644 --- a/packages/sdk/types/encoding.d.ts +++ b/packages/sdk/types/encoding.d.ts @@ -0,0 +1,2 @@ +import { StdTx } from "./types"; +export declare function marshalTx(tx: StdTx): Uint8Array; diff --git a/packages/sdk/types/index.d.ts b/packages/sdk/types/index.d.ts index f8dbdc95..4420e59b 100644 --- a/packages/sdk/types/index.d.ts +++ b/packages/sdk/types/index.d.ts @@ -1,2 +1,5 @@ +import * as types from "./types"; +export { unmarshalTx } from "./decoding"; +export { marshalTx } from "./encoding"; export { RestClient, TxsResponse } from "./restclient"; -export { AminoTx, isAminoStdTx } from "./types"; +export { types }; diff --git a/packages/sdk/types/restclient.d.ts b/packages/sdk/types/restclient.d.ts index ace2f82b..b818be1e 100644 --- a/packages/sdk/types/restclient.d.ts +++ b/packages/sdk/types/restclient.d.ts @@ -1,5 +1,4 @@ -import amino from "@tendermint/amino-js"; -import { AminoTx } from "./types"; +import { AminoTx, BaseAccount, StdTx } from "./types"; interface NodeInfo { readonly network: string; } @@ -27,7 +26,7 @@ interface BlocksResponse { } interface AuthAccountsResponse { readonly result: { - readonly value: amino.BaseAccount; + readonly value: BaseAccount; }; } export interface TxsResponse { @@ -51,13 +50,17 @@ interface PostTxsResponse { readonly code?: number; readonly raw_log?: string; } +interface EncodeTxResponse { + readonly tx: string; +} declare type RestClientResponse = | NodeInfoResponse | BlocksResponse | AuthAccountsResponse | TxsResponse | SearchTxsResponse - | PostTxsResponse; + | PostTxsResponse + | EncodeTxResponse; declare type BroadcastMode = "block" | "sync" | "async"; export declare class RestClient { private readonly client; @@ -68,6 +71,7 @@ export declare class RestClient { nodeInfo(): Promise; blocksLatest(): Promise; blocks(height: number): Promise; + encodeTx(stdTx: StdTx): Promise; authAccounts(address: string, height?: string): Promise; txs(query: string): Promise; txsById(id: string): Promise; diff --git a/packages/sdk/types/types.d.ts b/packages/sdk/types/types.d.ts index cb5a44a9..4f1bb326 100644 --- a/packages/sdk/types/types.d.ts +++ b/packages/sdk/types/types.d.ts @@ -1,5 +1,50 @@ -import amino from "@tendermint/amino-js"; -export declare type AminoTx = amino.Tx & { - readonly value: amino.StdTx; +export interface Tx { + readonly type: string; + readonly value: unknown; +} +export interface StdTx { + readonly msg: ReadonlyArray; + readonly fee: StdFee; + readonly signatures: ReadonlyArray; + readonly memo: string | undefined; +} +export declare type AminoTx = Tx & { + readonly value: StdTx; }; -export declare function isAminoStdTx(txValue: amino.TxValue): txValue is amino.StdTx; +export declare function isAminoStdTx(txValue: unknown): txValue is StdTx; +export interface Msg { + readonly type: string; + readonly value: MsgSend | unknown; +} +export interface MsgSend { + /** Bech32 account address */ + readonly from_address: string; + /** Bech32 account address */ + readonly to_address: string; + readonly amount: ReadonlyArray; +} +export interface StdFee { + readonly amount: ReadonlyArray; + readonly gas: string; +} +export interface Coin { + readonly denom: string; + readonly amount: string; +} +export interface StdSignature { + readonly pub_key: PubKey; + readonly signature: string; +} +export interface PubKey { + readonly type: string; + readonly value: string; +} +export declare type AccountPubKey = string; +export interface BaseAccount { + /** Bech32 account address */ + readonly address: string; + readonly coins: ReadonlyArray; + readonly public_key: AccountPubKey; + readonly account_number: string; + readonly sequence: string; +}