diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 00000000..697a5471 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,12 @@ +root = true + +[*] +charset = utf-8 +end_of_line = lf +indent_size = 2 +indent_style = space +insert_final_newline = true +trim_trailing_whitespace = true + +[*.py] +indent_size = 4 diff --git a/.eslintrc.json b/.eslintrc.json new file mode 100644 index 00000000..cbbaf4b5 --- /dev/null +++ b/.eslintrc.json @@ -0,0 +1,61 @@ +{ + "env": { + "es6": true, + "jasmine": true, + "node": true, + "worker": true + }, + "parser": "@typescript-eslint/parser", + "parserOptions": { + "ecmaVersion": 2018 + }, + "plugins": ["@typescript-eslint", "prettier", "simple-import-sort", "import"], + "extends": [ + "eslint:recommended", + "plugin:@typescript-eslint/recommended", + "prettier/@typescript-eslint", + "plugin:prettier/recommended", + "plugin:import/typescript" + ], + "rules": { + "curly": ["warn", "multi-line", "consistent"], + "no-console": ["warn", { "allow": ["error", "info", "warn"] }], + "no-param-reassign": "warn", + "no-shadow": "warn", + "prefer-const": "warn", + "spaced-comment": ["warn", "always", { "line": { "markers": ["/ { - const atom = "ATOM" as TokenTicker; + const cosm = "COSM" as TokenTicker; const httpUrl = "http://localhost:1317"; const defaultChainId = "cosmos:testing" as ChainId; const defaultEmptyAddress = "cosmos1h806c7khnvmjlywdrkdgk2vrayy2mmvf9rxk2r" as Address; @@ -40,10 +42,28 @@ describe("CosmosConnection", () => { const faucetPath = HdPaths.cosmos(0); const defaultRecipient = "cosmos1t70qnpr0az8tf7py83m4ue5y89w58lkjmx0yq2" as Address; + const defaultPrefix = "cosmos" as CosmosBech32Prefix; + + // this is for wasmd blockchain + const defaultTokens: TokenInfos = [ + { + fractionalDigits: 6, + tokenName: "Fee Token", + tokenTicker: "COSM" as TokenTicker, + denom: "cosm", + }, + { + fractionalDigits: 6, + tokenName: "Staking Token", + tokenTicker: "STAKE" as TokenTicker, + denom: "stake", + }, + ]; + describe("establish", () => { it("can connect to Cosmos via http", async () => { pendingWithoutCosmos(); - const connection = await CosmosConnection.establish(httpUrl); + const connection = await CosmosConnection.establish(httpUrl, defaultPrefix, defaultTokens); expect(connection).toBeTruthy(); connection.disconnect(); }); @@ -52,7 +72,7 @@ describe("CosmosConnection", () => { describe("chainId", () => { it("displays the chain ID", async () => { pendingWithoutCosmos(); - const connection = await CosmosConnection.establish(httpUrl); + const connection = await CosmosConnection.establish(httpUrl, defaultPrefix, defaultTokens); const chainId = connection.chainId(); expect(chainId).toEqual(defaultChainId); connection.disconnect(); @@ -62,7 +82,7 @@ describe("CosmosConnection", () => { describe("height", () => { it("displays the current height", async () => { pendingWithoutCosmos(); - const connection = await CosmosConnection.establish(httpUrl); + const connection = await CosmosConnection.establish(httpUrl, defaultPrefix, defaultTokens); const height = await connection.height(); expect(height).toBeGreaterThan(0); connection.disconnect(); @@ -72,19 +92,19 @@ describe("CosmosConnection", () => { describe("getToken", () => { it("displays a given token", async () => { pendingWithoutCosmos(); - const connection = await CosmosConnection.establish(httpUrl); - const token = await connection.getToken("cosm" as TokenTicker); + const connection = await CosmosConnection.establish(httpUrl, defaultPrefix, defaultTokens); + const token = await connection.getToken("COSM" as TokenTicker); expect(token).toEqual({ fractionalDigits: 6, - tokenName: "Cosm", - tokenTicker: "cosm" as TokenTicker, + tokenName: "Fee Token", + tokenTicker: "COSM" as TokenTicker, }); connection.disconnect(); }); it("resolves to undefined if the token is not supported", async () => { pendingWithoutCosmos(); - const connection = await CosmosConnection.establish(httpUrl); + const connection = await CosmosConnection.establish(httpUrl, defaultPrefix, defaultTokens); const token = await connection.getToken("whatever" as TokenTicker); expect(token).toBeUndefined(); connection.disconnect(); @@ -94,20 +114,20 @@ describe("CosmosConnection", () => { describe("getAllTokens", () => { it("resolves to a list of all supported tokens", async () => { pendingWithoutCosmos(); - const connection = await CosmosConnection.establish(httpUrl); + const connection = await CosmosConnection.establish(httpUrl, defaultPrefix, defaultTokens); const tokens = await connection.getAllTokens(); // TODO: make this more flexible expect(tokens).toEqual([ { fractionalDigits: 6, - tokenName: "Cosm", - tokenTicker: "cosm" as TokenTicker, + tokenName: "Fee Token", + tokenTicker: "COSM" as TokenTicker, }, { fractionalDigits: 6, - tokenName: "Stake", - tokenTicker: "stake" as TokenTicker, - } + tokenName: "Staking Token", + tokenTicker: "STAKE" as TokenTicker, + }, ]); connection.disconnect(); }); @@ -116,7 +136,7 @@ describe("CosmosConnection", () => { describe("getAccount", () => { it("gets an empty account by address", async () => { pendingWithoutCosmos(); - const connection = await CosmosConnection.establish(httpUrl); + const connection = await CosmosConnection.establish(httpUrl, defaultPrefix, defaultTokens); const account = await connection.getAccount({ address: defaultEmptyAddress }); expect(account).toBeUndefined(); connection.disconnect(); @@ -124,32 +144,31 @@ describe("CosmosConnection", () => { it("gets an account by address", async () => { pendingWithoutCosmos(); - const connection = await CosmosConnection.establish(httpUrl); + const connection = await CosmosConnection.establish(httpUrl, defaultPrefix, defaultTokens); const account = await connection.getAccount({ address: defaultAddress }); if (account === undefined) { throw new Error("Expected account not to be undefined"); } expect(account.address).toEqual(defaultAddress); - expect(account.pubkey).toEqual(defaultPubkey); - // Unsupported coins are filtered out - expect(account.balance.length).toEqual(1); + // Undefined until we sign a transaction + expect(account.pubkey).toEqual(undefined); + // Starts with two tokens + expect(account.balance.length).toEqual(2); connection.disconnect(); }); it("gets an account by pubkey", async () => { pendingWithoutCosmos(); - const connection = await CosmosConnection.establish(httpUrl); + const connection = await CosmosConnection.establish(httpUrl, defaultPrefix, defaultTokens); const account = await connection.getAccount({ pubkey: defaultPubkey }); if (account === undefined) { throw new Error("Expected account not to be undefined"); } expect(account.address).toEqual(defaultAddress); - expect(account.pubkey).toEqual({ - algo: Algorithm.Secp256k1, - data: fromBase64("A08EGB7ro1ORuFhjOnZcSgwYlpe0DSFjVNUIkNNQxwKQ"), - }); - // Unsupported coins are filtered out - expect(account.balance.length).toEqual(1); + // Undefined until we sign a transaction + expect(account.pubkey).toEqual(undefined); + // Starts with two tokens + expect(account.balance.length).toEqual(2); connection.disconnect(); }); }); @@ -157,7 +176,7 @@ describe("CosmosConnection", () => { describe("integration tests", () => { it("can post and get a transaction", async () => { pendingWithoutCosmos(); - const connection = await CosmosConnection.establish(httpUrl); + const connection = await CosmosConnection.establish(httpUrl, defaultPrefix, defaultTokens); const profile = new UserProfile(); const wallet = profile.addWallet(Secp256k1HdWallet.fromMnemonic(faucetMnemonic)); const faucet = await profile.createIdentity(wallet.id, defaultChainId, faucetPath); @@ -172,13 +191,17 @@ describe("CosmosConnection", () => { amount: { quantity: "75000", fractionalDigits: 6, - tokenTicker: atom, + tokenTicker: cosm, }, }); const nonce = await connection.getNonce({ address: faucetAddress }); - const signed = await profile.signTransaction(faucet, unsigned, cosmosCodec, nonce); - const postableBytes = cosmosCodec.bytesToPost(signed); + // TODO: we need to use custom codecs everywhere + const codec = new CosmosCodec(defaultPrefix, defaultTokens); + console.log("nonce:", nonce); + const signed = await profile.signTransaction(faucet, unsigned, codec, nonce); + const postableBytes = codec.bytesToPost(signed); const response = await connection.postTx(postableBytes); + console.log(response); const { transactionId } = response; const blockInfo = await response.blockInfo.waitFor(info => !isBlockInfoPending(info)); expect(blockInfo.state).toEqual(TransactionState.Succeeded); @@ -216,7 +239,7 @@ describe("CosmosConnection", () => { it("can post and search for a transaction", async () => { pendingWithoutCosmos(); - const connection = await CosmosConnection.establish(httpUrl); + const connection = await CosmosConnection.establish(httpUrl, defaultPrefix, defaultTokens); const profile = new UserProfile(); const wallet = profile.addWallet(Secp256k1HdWallet.fromMnemonic(faucetMnemonic)); const faucet = await profile.createIdentity(wallet.id, defaultChainId, faucetPath); @@ -231,13 +254,16 @@ describe("CosmosConnection", () => { amount: { quantity: "75000", fractionalDigits: 6, - tokenTicker: atom, + tokenTicker: cosm, }, }); const nonce = await connection.getNonce({ address: faucetAddress }); - const signed = await profile.signTransaction(faucet, unsigned, cosmosCodec, nonce); - const postableBytes = cosmosCodec.bytesToPost(signed); + // TODO: we need to use custom codecs everywhere + const codec = new CosmosCodec(defaultPrefix, defaultTokens); + const signed = await profile.signTransaction(faucet, unsigned, codec, nonce); + const postableBytes = codec.bytesToPost(signed); const response = await connection.postTx(postableBytes); + console.log(response); const { transactionId } = response; const blockInfo = await response.blockInfo.waitFor(info => !isBlockInfoPending(info)); expect(blockInfo.state).toEqual(TransactionState.Succeeded); diff --git a/src/cosmosconnection.ts b/src/cosmosconnection.ts index 45460485..76f67f45 100644 --- a/src/cosmosconnection.ts +++ b/src/cosmosconnection.ts @@ -38,6 +38,7 @@ import { CosmosBech32Prefix, pubkeyToAddress } from "./address"; import { Caip5 } from "./caip5"; import { decodeAmount, parseTxsResponse } from "./decode"; import { RestClient, TxsResponse } from "./restclient"; +import { TokenInfos } from "./types"; const { fromBase64 } = Encoding; @@ -70,10 +71,15 @@ function buildQueryString({ } export class CosmosConnection implements BlockchainConnection { - public static async establish(url: string): Promise { + // we must know prefix and tokens a priori to understand the chain + public static async establish( + url: string, + prefix: CosmosBech32Prefix, + tokenInfo: TokenInfos, + ): Promise { const restClient = new RestClient(url); const chainData = await this.initialize(restClient); - return new CosmosConnection(restClient, chainData); + return new CosmosConnection(restClient, chainData, prefix, tokenInfo); } private static async initialize(restClient: RestClient): Promise { @@ -84,26 +90,34 @@ export class CosmosConnection implements BlockchainConnection { private readonly restClient: RestClient; private readonly chainData: ChainData; private readonly primaryToken: Token; + + // TODO: deprecate this??? private readonly supportedTokens: readonly Token[]; + private readonly _prefix: CosmosBech32Prefix; + private readonly tokenInfo: TokenInfos; + private get prefix(): CosmosBech32Prefix { - return "cosmos"; + return this._prefix; } - private constructor(restClient: RestClient, chainData: ChainData) { + private constructor( + restClient: RestClient, + chainData: ChainData, + prefix: CosmosBech32Prefix, + tokenInfo: TokenInfos, + ) { this.restClient = restClient; this.chainData = chainData; - // TODO: this is an argument - this.primaryToken = { - fractionalDigits: 6, - tokenName: "Cosm", - tokenTicker: "cosm" as TokenTicker, - }; - this.supportedTokens = [this.primaryToken, { - fractionalDigits: 6, - tokenName: "Stake", - tokenTicker: "stake" as TokenTicker, - }]; + this._prefix = prefix; + this.tokenInfo = tokenInfo; + + this.supportedTokens = this.tokenInfo.map(info => ({ + tokenTicker: info.tokenTicker, + tokenName: info.tokenName, + fractionalDigits: info.fractionalDigits, + })); + this.primaryToken = this.supportedTokens[0]; } public disconnect(): void { @@ -131,22 +145,23 @@ export class CosmosConnection implements BlockchainConnection { const address = isPubkeyQuery(query) ? pubkeyToAddress(query.pubkey, this.prefix) : query.address; const { result } = await this.restClient.authAccounts(address); const account = result.value; + if (!account.address) { + return undefined; + } const supportedCoins = account.coins.filter(({ denom }) => - this.supportedTokens.find( - // TODO: ugly special case - fix this - ({ tokenTicker }) => (tokenTicker === "ATOM" && denom === "uatom") || tokenTicker === denom, - ), + this.tokenInfo.find(token => token.denom === denom), ); - return account.public_key === null + const pubkey = !account.public_key ? undefined : { - address: address, - balance: supportedCoins.map(decodeAmount), - pubkey: { - algo: Algorithm.Secp256k1, - data: fromBase64(account.public_key.value) as PubkeyBytes, - }, + algo: Algorithm.Secp256k1, + data: fromBase64(account.public_key.value) as PubkeyBytes, }; + return { + address: address, + balance: supportedCoins.map(decodeAmount(this.tokenInfo)), + pubkey: pubkey, + }; } public watchAccount(_account: AccountQuery): Stream { @@ -199,6 +214,7 @@ export class CosmosConnection implements BlockchainConnection { } public async postTx(tx: PostableBytes): Promise { + // TODO: we need to check errors here... bad chain-id breaks this const { txhash, raw_log } = await this.restClient.postTx(tx); const transactionId = txhash as TransactionId; const firstEvent: BlockInfo = { state: TransactionState.Pending }; @@ -286,6 +302,6 @@ export class CosmosConnection implements BlockchainConnection { const sender = (response.tx.value as any).msg[0].value.from_address; const accountForHeight = await this.restClient.authAccounts(sender, response.height); const nonce = (parseInt(accountForHeight.result.value.sequence, 10) - 1) as Nonce; - return parseTxsResponse(chainId, parseInt(response.height, 10), nonce, response); + return parseTxsResponse(chainId, parseInt(response.height, 10), nonce, response, this.tokenInfo); } } diff --git a/src/cosmosconnector.ts b/src/cosmosconnector.ts index 7e3d4ccd..e4b13716 100644 --- a/src/cosmosconnector.ts +++ b/src/cosmosconnector.ts @@ -2,16 +2,20 @@ import { ChainConnector, ChainId } from "@iov/bcp"; import { cosmosCodec } from "./cosmoscodec"; import { CosmosConnection } from "./cosmosconnection"; +import { CosmosBech32Prefix } from "./address"; +import { TokenInfos } from "./types"; /** * A helper to connect to a cosmos-based chain at a given url */ export function createCosmosConnector( url: string, + prefix: CosmosBech32Prefix, + tokenInfo: TokenInfos, expectedChainId?: ChainId, ): ChainConnector { return { - establishConnection: async () => CosmosConnection.establish(url), + establishConnection: async () => CosmosConnection.establish(url, prefix, tokenInfo), codec: cosmosCodec, expectedChainId: expectedChainId, }; diff --git a/src/decode.spec.ts b/src/decode.spec.ts index 52f29ab7..64fe28f4 100644 --- a/src/decode.spec.ts +++ b/src/decode.spec.ts @@ -15,6 +15,7 @@ import { } from "./decode"; import { chainId, nonce, signedTxJson, txId } from "./testdata.spec"; import data from "./testdata/cosmoshub.json"; +import { TokenInfos } from "./types"; const { fromBase64 } = Encoding; @@ -51,6 +52,14 @@ describe("decode", () => { }, gasLimit: "200000", }; + const defaultTokens: TokenInfos = [ + { + fractionalDigits: 6, + tokenName: "Atom (Cosmos Hub)", + tokenTicker: "ATOM" as TokenTicker, + denom: "uatom", + }, + ]; describe("decodePubkey", () => { it("works", () => { @@ -89,7 +98,7 @@ describe("decode", () => { denom: "uatom", amount: "11657995", }; - expect(decodeAmount(amount)).toEqual(defaultAmount); + expect(decodeAmount(defaultTokens)(amount)).toEqual(defaultAmount); }); }); @@ -108,7 +117,7 @@ describe("decode", () => { ], }, }; - expect(parseMsg(msg, chainId)).toEqual(defaultSendTransaction); + expect(parseMsg(msg, chainId, defaultTokens)).toEqual(defaultSendTransaction); }); }); @@ -123,13 +132,13 @@ describe("decode", () => { ], gas: "200000", }; - expect(parseFee(fee)).toEqual(defaultFee); + expect(parseFee(fee, defaultTokens)).toEqual(defaultFee); }); }); describe("parseTx", () => { it("works", () => { - expect(parseTx(data.tx, chainId, nonce)).toEqual(signedTxJson); + expect(parseTx(data.tx, chainId, nonce, defaultTokens)).toEqual(signedTxJson); }); }); @@ -149,7 +158,7 @@ describe("decode", () => { transactionId: txId, log: '[{"msg_index":0,"success":true,"log":""}]', }; - expect(parseTxsResponse(chainId, currentHeight, nonce, txsResponse)).toEqual(expected); + expect(parseTxsResponse(chainId, currentHeight, nonce, txsResponse, defaultTokens)).toEqual(expected); }); }); }); diff --git a/src/decode.ts b/src/decode.ts index c368c804..af0f2187 100644 --- a/src/decode.ts +++ b/src/decode.ts @@ -20,7 +20,7 @@ import { Encoding } from "@iov/encoding"; import amino from "@tendermint/amino-js"; import { TxsResponse } from "./restclient"; -import { isAminoStdTx } from "./types"; +import { isAminoStdTx, TokenInfos, coinToAmount } from "./types"; const { fromBase64 } = Encoding; @@ -45,20 +45,15 @@ export function decodeFullSignature(signature: amino.StdSignature, nonce: number }; } -export function decodeAmount(amount: amino.Coin): Amount { - // TODO: more uglyness here (breaks unit tests) - if (amount.denom !== "uatom") { - throw new Error("Only ATOM amounts are supported"); - } - return { - fractionalDigits: 6, - quantity: amount.amount, - tokenTicker: atom, - // tokenTicker: amount.denom as TokenTicker, - }; -} +// TODO: this needs access to token list - we need something more like amountToCoin and coinToAmount here +// and wire that info all the way from both connection and codec. -export function parseMsg(msg: amino.Msg, chainId: ChainId): SendTransaction { +// TODO: return null vs throw exception for undefined??? +export const decodeAmount = (tokens: TokenInfos) => (coin: amino.Coin): Amount => { + return coinToAmount(tokens, coin); +}; + +export function parseMsg(msg: amino.Msg, chainId: ChainId, tokens: TokenInfos): SendTransaction { if (msg.type !== "cosmos-sdk/MsgSend") { throw new Error("Unknown message type in transaction"); } @@ -74,21 +69,22 @@ export function parseMsg(msg: amino.Msg, chainId: ChainId): SendTransaction { chainId: chainId, sender: msgValue.from_address as Address, recipient: msgValue.to_address as Address, - amount: decodeAmount(msgValue.amount[0]), + // TODO: this needs access to token list + amount: decodeAmount(tokens)(msgValue.amount[0]), }; } -export function parseFee(fee: amino.StdFee): Fee { +export function parseFee(fee: amino.StdFee, tokens: TokenInfos): Fee { if (fee.amount.length !== 1) { throw new Error("Only fee with one amount is supported"); } return { - tokens: decodeAmount(fee.amount[0]), + tokens: decodeAmount(tokens)(fee.amount[0]), gasLimit: fee.gas, }; } -export function parseTx(tx: amino.Tx, chainId: ChainId, nonce: Nonce): SignedTransaction { +export function parseTx(tx: amino.Tx, chainId: ChainId, nonce: Nonce, tokens: TokenInfos): SignedTransaction { const txValue = tx.value; if (!isAminoStdTx(txValue)) { throw new Error("Only Amino StdTx is supported"); @@ -98,8 +94,10 @@ export function parseTx(tx: amino.Tx, chainId: ChainId, nonce: Nonce): SignedTra } const [primarySignature] = txValue.signatures.map(signature => decodeFullSignature(signature, nonce)); - const msg = parseMsg(txValue.msg[0], chainId); - const fee = parseFee(txValue.fee); + // TODO: this needs access to token list + const msg = parseMsg(txValue.msg[0], chainId, tokens); + // TODO: this needs access to token list + const fee = parseFee(txValue.fee, tokens); const transaction = { ...msg, @@ -119,10 +117,11 @@ export function parseTxsResponse( currentHeight: number, nonce: Nonce, response: TxsResponse, + tokens: TokenInfos, ): ConfirmedAndSignedTransaction { const height = parseInt(response.height, 10); return { - ...parseTx(response.tx, chainId, nonce), + ...parseTx(response.tx, chainId, nonce, tokens), height: height, confirmations: currentHeight - height + 1, transactionId: response.txhash as TransactionId, diff --git a/src/encode.spec.ts b/src/encode.spec.ts index 4b4179e5..2eaaeb14 100644 --- a/src/encode.spec.ts +++ b/src/encode.spec.ts @@ -21,6 +21,7 @@ import { encodeFullSignature, encodePubkey, } from "./encode"; +import { TokenInfos } from "./types"; const { fromBase64 } = Encoding; @@ -40,6 +41,14 @@ describe("encode", () => { tokenTicker: atom, }; const defaultMemo = "hello cosmos hub"; + const defaultTokens: TokenInfos = [ + { + fractionalDigits: 6, + tokenName: "Atom (Cosmos Hub)", + tokenTicker: "ATOM" as TokenTicker, + denom: "uatom", + }, + ]; describe("encodePubKey", () => { it("encodes a Secp256k1 pubkey", () => { @@ -52,7 +61,7 @@ describe("encode", () => { describe("encodeAmount", () => { it("encodes an amount", () => { - expect(encodeAmount(defaultAmount)).toEqual({ + expect(encodeAmount(defaultAmount, defaultTokens)).toEqual({ denom: "uatom", amount: "11657995", }); @@ -64,7 +73,7 @@ describe("encode", () => { const fee = { gasLimit: "200000", }; - expect(() => encodeFee(fee)).toThrowError(/cannot encode fee without tokens/i); + expect(() => encodeFee(fee, defaultTokens)).toThrowError(/cannot encode fee without tokens/i); }); it("throws without gas limit", () => { @@ -75,7 +84,7 @@ describe("encode", () => { tokenTicker: atom, }, }; - expect(() => encodeFee(fee)).toThrowError(/cannot encode fee without gas limit/i); + expect(() => encodeFee(fee, defaultTokens)).toThrowError(/cannot encode fee without gas limit/i); }); it("encodes a fee", () => { @@ -87,7 +96,7 @@ describe("encode", () => { }, gasLimit: "200000", }; - expect(encodeFee(fee)).toEqual({ + expect(encodeFee(fee, defaultTokens)).toEqual({ amount: [{ denom: "uatom", amount: "5000" }], gas: "200000", }); @@ -168,7 +177,9 @@ describe("encode", () => { chainId: defaultChainId, escrowId: "defg", }; - expect(() => buildUnsignedTx(tx)).toThrowError(/received transaction of unsupported kind/i); + expect(() => buildUnsignedTx(tx, defaultTokens)).toThrowError( + /received transaction of unsupported kind/i, + ); }); it("builds a send transaction without fee", () => { @@ -180,7 +191,7 @@ describe("encode", () => { recipient: defaultRecipient, memo: defaultMemo, }; - expect(buildUnsignedTx(tx)).toEqual({ + expect(buildUnsignedTx(tx, defaultTokens)).toEqual({ type: "cosmos-sdk/StdTx", value: { msg: [ @@ -225,7 +236,7 @@ describe("encode", () => { gasLimit: "200000", }, }; - expect(buildUnsignedTx(tx)).toEqual({ + expect(buildUnsignedTx(tx, defaultTokens)).toEqual({ type: "cosmos-sdk/StdTx", value: { msg: [ @@ -286,7 +297,7 @@ describe("encode", () => { }, ], }; - expect(buildSignedTx(tx)).toEqual({ + expect(buildSignedTx(tx, defaultTokens)).toEqual({ type: "cosmos-sdk/StdTx", value: { msg: [ diff --git a/src/encode.ts b/src/encode.ts index 88ddb24d..1c098084 100644 --- a/src/encode.ts +++ b/src/encode.ts @@ -13,7 +13,7 @@ import { Secp256k1 } from "@iov/crypto"; import { Encoding } from "@iov/encoding"; import amino from "@tendermint/amino-js"; -import { AminoTx } from "./types"; +import { AminoTx, TokenInfos, amountToCoin } from "./types"; const { toBase64 } = Encoding; @@ -34,17 +34,11 @@ export function encodePubkey(pubkey: PubkeyBundle): amino.PubKey { } } -export function encodeAmount(amount: Amount): amino.Coin { - if (amount.tokenTicker !== "ATOM") { - throw new Error("Only ATOM amounts are supported"); - } - return { - denom: "uatom", - amount: amount.quantity, - }; +export function encodeAmount(amount: Amount, tokens: TokenInfos): amino.Coin { + return amountToCoin(tokens, amount); } -export function encodeFee(fee: Fee): amino.StdFee { +export function encodeFee(fee: Fee, tokens: TokenInfos): amino.StdFee { if (fee.tokens === undefined) { throw new Error("Cannot encode fee without tokens"); } @@ -52,7 +46,7 @@ export function encodeFee(fee: Fee): amino.StdFee { throw new Error("Cannot encode fee without gas limit"); } return { - amount: [encodeAmount(fee.tokens)], + amount: [encodeAmount(fee.tokens, tokens)], gas: fee.gasLimit, }; } @@ -68,7 +62,7 @@ export function encodeFullSignature(fullSignature: FullSignature): amino.StdSign }; } -export function buildUnsignedTx(tx: UnsignedTransaction): AminoTx { +export function buildUnsignedTx(tx: UnsignedTransaction, tokens: TokenInfos): AminoTx { if (!isSendTransaction(tx)) { throw new Error("Received transaction of unsupported kind"); } @@ -81,14 +75,14 @@ export function buildUnsignedTx(tx: UnsignedTransaction): AminoTx { value: { from_address: tx.sender, to_address: tx.recipient, - amount: [encodeAmount(tx.amount)], + amount: [encodeAmount(tx.amount, tokens)], }, }, ], memo: tx.memo || "", signatures: [], fee: tx.fee - ? encodeFee(tx.fee) + ? encodeFee(tx.fee, tokens) : { amount: [], gas: "", @@ -97,8 +91,8 @@ export function buildUnsignedTx(tx: UnsignedTransaction): AminoTx { }; } -export function buildSignedTx(tx: SignedTransaction): AminoTx { - const built = buildUnsignedTx(tx.transaction); +export function buildSignedTx(tx: SignedTransaction, tokens: TokenInfos): AminoTx { + const built = buildUnsignedTx(tx.transaction, tokens); return { ...built, value: { diff --git a/src/types.ts b/src/types.ts index c8a0ef72..03996413 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,3 +1,4 @@ +import { Amount, Token } from "@iov/bcp"; import amino from "@tendermint/amino-js"; export type AminoTx = amino.Tx & { readonly value: amino.StdTx }; @@ -8,3 +9,33 @@ export function isAminoStdTx(txValue: amino.TxValue): txValue is amino.StdTx { typeof memo === "string" && Array.isArray(msg) && typeof fee === "object" && Array.isArray(signatures) ); } + +export interface TokenInfo extends Token { + readonly denom: string; +} + +export type TokenInfos = ReadonlyArray; + +// TODO: alias amino types +export function amountToCoin(lookup: ReadonlyArray, amount: Amount): amino.Coin { + const match = lookup.find(({ tokenTicker }) => tokenTicker === amount.tokenTicker); + if (!match) { + throw Error(`unknown ticker: ${amount.tokenTicker}`); + } + return { + denom: match.denom, + amount: amount.quantity, + }; +} + +export function coinToAmount(tokens: TokenInfos, coin: amino.Coin): Amount { + const match = tokens.find(({ denom }) => denom === coin.denom); + if (!match) { + throw Error(`unknown denom: ${coin.denom}`); + } + return { + tokenTicker: match.tokenTicker, + fractionalDigits: match.fractionalDigits, + quantity: coin.amount, + }; +} diff --git a/types/address.d.ts b/types/address.d.ts index c5cba906..776cb61d 100644 --- a/types/address.d.ts +++ b/types/address.d.ts @@ -1,23 +1,12 @@ import { Address, PubkeyBundle } from "@iov/bcp"; -export declare type CosmosAddressBech32Prefix = - | "cosmos" - | "cosmosvalcons" - | "cosmosvaloper"; -export declare type CosmosPubkeyBech32Prefix = - | "cosmospub" - | "cosmosvalconspub" - | "cosmosvaloperpub"; -export declare type CosmosBech32Prefix = - | CosmosAddressBech32Prefix - | CosmosPubkeyBech32Prefix; +export declare type CosmosAddressBech32Prefix = "cosmos" | "cosmosvalcons" | "cosmosvaloper"; +export declare type CosmosPubkeyBech32Prefix = "cosmospub" | "cosmosvalconspub" | "cosmosvaloperpub"; +export declare type CosmosBech32Prefix = CosmosAddressBech32Prefix | CosmosPubkeyBech32Prefix; export declare function decodeCosmosAddress( - address: Address + address: Address, ): { readonly prefix: CosmosAddressBech32Prefix; readonly data: Uint8Array; }; export declare function isValidAddress(address: string): boolean; -export declare function pubkeyToAddress( - pubkey: PubkeyBundle, - prefix: CosmosBech32Prefix -): Address; +export declare function pubkeyToAddress(pubkey: PubkeyBundle, prefix: CosmosBech32Prefix): Address; diff --git a/types/cosmoscodec.d.ts b/types/cosmoscodec.d.ts index 59482edc..64de7f38 100644 --- a/types/cosmoscodec.d.ts +++ b/types/cosmoscodec.d.ts @@ -8,17 +8,18 @@ import { SigningJob, TransactionId, TxCodec, - UnsignedTransaction + UnsignedTransaction, } from "@iov/bcp"; +import { CosmosBech32Prefix } from "./address"; +import { TokenInfos } from "./types"; export declare class CosmosCodec implements TxCodec { + private readonly prefix; + private readonly tokens; + constructor(prefix: CosmosBech32Prefix, tokens: TokenInfos); bytesToSign(unsigned: UnsignedTransaction, nonce: Nonce): SigningJob; bytesToPost(signed: SignedTransaction): PostableBytes; identifier(signed: SignedTransaction): TransactionId; - parseBytes( - bytes: PostableBytes, - chainId: ChainId, - nonce?: Nonce - ): SignedTransaction; + parseBytes(bytes: PostableBytes, chainId: ChainId, nonce?: Nonce): SignedTransaction; identityToAddress(identity: Identity): Address; isValidAddress(address: string): boolean; } diff --git a/types/cosmosconnection.d.ts b/types/cosmosconnection.d.ts index 94efb242..4c8e8f66 100644 --- a/types/cosmosconnection.d.ts +++ b/types/cosmosconnection.d.ts @@ -17,16 +17,20 @@ import { TokenTicker, TransactionId, TransactionQuery, - UnsignedTransaction + UnsignedTransaction, } from "@iov/bcp"; import { Stream } from "xstream"; +import { CosmosBech32Prefix } from "./address"; +import { TokenInfos } from "./types"; export declare class CosmosConnection implements BlockchainConnection { - static establish(url: string): Promise; + static establish(url: string, prefix: CosmosBech32Prefix, tokenInfo: TokenInfos): Promise; private static initialize; private readonly restClient; private readonly chainData; private readonly primaryToken; private readonly supportedTokens; + private readonly _prefix; + private readonly tokenInfo; private get prefix(); private constructor(); disconnect(): void; @@ -37,29 +41,16 @@ export declare class CosmosConnection implements BlockchainConnection { getAccount(query: AccountQuery): Promise; watchAccount(_account: AccountQuery): Stream; getNonce(query: AddressQuery | PubkeyQuery): Promise; - getNonces( - query: AddressQuery | PubkeyQuery, - count: number - ): Promise; + getNonces(query: AddressQuery | PubkeyQuery, count: number): Promise; getBlockHeader(height: number): Promise; watchBlockHeaders(): Stream; - getTx( - id: TransactionId - ): Promise< - ConfirmedAndSignedTransaction | FailedTransaction - >; + getTx(id: TransactionId): Promise | FailedTransaction>; postTx(tx: PostableBytes): Promise; searchTx( - query: TransactionQuery - ): Promise< - readonly (ConfirmedTransaction | FailedTransaction)[] - >; - listenTx( - _query: TransactionQuery - ): Stream | FailedTransaction>; - liveTx( - _query: TransactionQuery - ): Stream | FailedTransaction>; + query: TransactionQuery, + ): Promise | FailedTransaction)[]>; + listenTx(_query: TransactionQuery): Stream | FailedTransaction>; + liveTx(_query: TransactionQuery): Stream | FailedTransaction>; getFeeQuote(tx: UnsignedTransaction): Promise; withDefaultFee(tx: T): Promise; private parseAndPopulateTxResponse; diff --git a/types/cosmosconnector.d.ts b/types/cosmosconnector.d.ts index 6a18915d..509a8068 100644 --- a/types/cosmosconnector.d.ts +++ b/types/cosmosconnector.d.ts @@ -1,9 +1,13 @@ import { ChainConnector, ChainId } from "@iov/bcp"; import { CosmosConnection } from "./cosmosconnection"; +import { CosmosBech32Prefix } from "./address"; +import { TokenInfos } from "./types"; /** * A helper to connect to a cosmos-based chain at a given url */ export declare function createCosmosConnector( url: string, - expectedChainId?: ChainId + prefix: CosmosBech32Prefix, + tokenInfo: TokenInfos, + expectedChainId?: ChainId, ): ChainConnector; diff --git a/types/decode.d.ts b/types/decode.d.ts index ac66c871..8b7332c7 100644 --- a/types/decode.d.ts +++ b/types/decode.d.ts @@ -9,30 +9,27 @@ import { SendTransaction, SignatureBytes, SignedTransaction, - UnsignedTransaction + UnsignedTransaction, } from "@iov/bcp"; import amino from "@tendermint/amino-js"; import { TxsResponse } from "./restclient"; +import { TokenInfos } from "./types"; export declare function decodePubkey(pubkey: amino.PubKey): PubkeyBundle; export declare function decodeSignature(signature: string): SignatureBytes; -export declare function decodeFullSignature( - signature: amino.StdSignature, - nonce: number -): FullSignature; -export declare function decodeAmount(amount: amino.Coin): Amount; -export declare function parseMsg( - msg: amino.Msg, - chainId: ChainId -): SendTransaction; -export declare function parseFee(fee: amino.StdFee): Fee; +export declare function decodeFullSignature(signature: amino.StdSignature, nonce: number): FullSignature; +export declare const 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 parseTx( tx: amino.Tx, chainId: ChainId, - nonce: Nonce + nonce: Nonce, + tokens: TokenInfos, ): SignedTransaction; export declare function parseTxsResponse( chainId: ChainId, currentHeight: number, nonce: Nonce, - response: TxsResponse + response: TxsResponse, + tokens: TokenInfos, ): ConfirmedAndSignedTransaction; diff --git a/types/encode.d.ts b/types/encode.d.ts index e5931aef..2a0788df 100644 --- a/types/encode.d.ts +++ b/types/encode.d.ts @@ -1,18 +1,9 @@ -import { - Amount, - Fee, - FullSignature, - PubkeyBundle, - SignedTransaction, - UnsignedTransaction -} from "@iov/bcp"; +import { Amount, Fee, FullSignature, PubkeyBundle, SignedTransaction, UnsignedTransaction } from "@iov/bcp"; import amino from "@tendermint/amino-js"; -import { AminoTx } from "./types"; +import { AminoTx, TokenInfos } from "./types"; export declare function encodePubkey(pubkey: PubkeyBundle): amino.PubKey; -export declare function encodeAmount(amount: Amount): amino.Coin; -export declare function encodeFee(fee: Fee): amino.StdFee; -export declare function encodeFullSignature( - fullSignature: FullSignature -): amino.StdSignature; -export declare function buildUnsignedTx(tx: UnsignedTransaction): AminoTx; -export declare function buildSignedTx(tx: SignedTransaction): AminoTx; +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; diff --git a/types/restclient.d.ts b/types/restclient.d.ts index 9097d2cb..df1d9251 100644 --- a/types/restclient.d.ts +++ b/types/restclient.d.ts @@ -69,10 +69,7 @@ export declare class RestClient { nodeInfo(): Promise; blocksLatest(): Promise; blocks(height: number): Promise; - authAccounts( - address: Address, - height?: string - ): Promise; + authAccounts(address: Address, height?: string): Promise; txs(query: string): Promise; txsById(id: TransactionId): Promise; postTx(tx: PostableBytes): Promise; diff --git a/types/types.d.ts b/types/types.d.ts index adcda576..62d5ab9b 100644 --- a/types/types.d.ts +++ b/types/types.d.ts @@ -1,7 +1,12 @@ +import { Amount, Token } from "@iov/bcp"; import amino from "@tendermint/amino-js"; export declare type AminoTx = amino.Tx & { readonly value: amino.StdTx; }; -export declare function isAminoStdTx( - txValue: amino.TxValue -): txValue is amino.StdTx; +export declare function isAminoStdTx(txValue: amino.TxValue): txValue is amino.StdTx; +export interface TokenInfo extends Token { + readonly denom: string; +} +export declare type TokenInfos = ReadonlyArray; +export declare function amountToCoin(lookup: ReadonlyArray, amount: Amount): amino.Coin; +export declare function coinToAmount(tokens: TokenInfos, coin: amino.Coin): Amount;