diff --git a/packages/bcp/src/cosmwasmcodec.ts b/packages/bcp/src/cosmwasmcodec.ts index 625b285b..d2964397 100644 --- a/packages/bcp/src/cosmwasmcodec.ts +++ b/packages/bcp/src/cosmwasmcodec.ts @@ -23,7 +23,7 @@ import { import { pubkeyToAddress } from "./address"; import { Caip5 } from "./caip5"; -import { parseTx } from "./decode"; +import { parseSignedTx } from "./decode"; import { buildSignedTx, buildUnsignedTx } from "./encode"; import { BankTokens, Erc20Token, nonceToAccountNumber, nonceToSequence } from "./types"; @@ -81,7 +81,7 @@ export class CosmWasmCodec implements TxCodec { throw new Error("Nonce is required"); } const parsed = unmarshalTx(bytes); - return parseTx(parsed, chainId, nonce, this.bankTokens); + return parseSignedTx(parsed, chainId, nonce, this.bankTokens); } public identityToAddress(identity: Identity): Address { diff --git a/packages/bcp/src/cosmwasmconnection.ts b/packages/bcp/src/cosmwasmconnection.ts index 09aea072..d7c9e893 100644 --- a/packages/bcp/src/cosmwasmconnection.ts +++ b/packages/bcp/src/cosmwasmconnection.ts @@ -38,7 +38,7 @@ import { Stream } from "xstream"; import { decodeCosmosPubkey, pubkeyToAddress } from "./address"; import { Caip5 } from "./caip5"; -import { decodeAmount, parseTxsResponse } from "./decode"; +import { decodeAmount, parseTxsResponseSigned, parseTxsResponseUnsigned } from "./decode"; import { buildSignedTx } from "./encode"; import { accountToNonce, BankToken, Erc20Token } from "./types"; @@ -234,8 +234,7 @@ export class CosmWasmConnection implements BlockchainConnection { try { // tslint:disable-next-line: deprecation const response = await this.restClient.txsById(id); - const chainId = this.chainId(); - return this.parseAndPopulateTxResponse(response, chainId); + return this.parseAndPopulateTxResponseSigned(response); } catch (error) { if (error.response.status === 404) { throw new Error("Transaction does not exist"); @@ -319,8 +318,7 @@ export class CosmWasmConnection implements BlockchainConnection { throw new Error("Unsupported query"); } - const chainId = this.chainId(); - return Promise.all(txs.map(tx => this.parseAndPopulateTxResponse(tx, chainId))); + return txs.map(tx => this.parseAndPopulateTxResponseUnsigned(tx)); } public listenTx( @@ -357,34 +355,42 @@ export class CosmWasmConnection implements BlockchainConnection { }; } - private async parseAndPopulateTxResponse( + private parseAndPopulateTxResponseUnsigned( + response: TxsResponse, + ): ConfirmedTransaction | FailedTransaction { + const chainId = this.chainId(); + return parseTxsResponseUnsigned(chainId, parseInt(response.height, 10), response, this.bankTokens); + } + + private async parseAndPopulateTxResponseSigned( response: TxsResponse, - chainId: ChainId, ): Promise | 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; + // needed to get the (account_number, sequence) for the primary signature + let primarySignerAddress: string; if (types.isMsgSend(firstMsg)) { - senderAddress = firstMsg.value.from_address; + primarySignerAddress = firstMsg.value.from_address; } else if ( types.isMsgStoreCode(firstMsg) || types.isMsgInstantiateContract(firstMsg) || types.isMsgExecuteContract(firstMsg) ) { - senderAddress = firstMsg.value.sender; + primarySignerAddress = firstMsg.value.sender; } else { throw new Error(`Got unsupported type of message: ${firstMsg.type}`); } // tslint:disable-next-line: deprecation - const accountForHeight = await this.restClient.authAccounts(senderAddress, response.height); + const accountForHeight = await this.restClient.authAccounts(primarySignerAddress, response.height); const accountNumber = accountForHeight.result.value.account_number; // this is technically not the proper sequence. maybe this causes issues for sig validation? // leaving for now unless it causes issues const sequence = accountForHeight.result.value.sequence - 1; const nonce = accountToNonce(accountNumber, sequence); - return parseTxsResponse(chainId, parseInt(response.height, 10), nonce, response, this.bankTokens); + const chainId = this.chainId(); + return parseTxsResponseSigned(chainId, parseInt(response.height, 10), nonce, response, this.bankTokens); } } diff --git a/packages/bcp/src/decode.spec.ts b/packages/bcp/src/decode.spec.ts index 5f48b6f3..ce04f912 100644 --- a/packages/bcp/src/decode.spec.ts +++ b/packages/bcp/src/decode.spec.ts @@ -1,6 +1,6 @@ /* eslint-disable @typescript-eslint/camelcase */ import { types } from "@cosmwasm/sdk"; -import { Address, Algorithm, TokenTicker } from "@iov/bcp"; +import { Address, Algorithm, SendTransaction, TokenTicker } from "@iov/bcp"; import { Encoding } from "@iov/encoding"; import { @@ -10,11 +10,13 @@ import { decodeSignature, parseFee, parseMsg, - parseTx, - parseTxsResponse, + parseSignedTx, + parseTxsResponseSigned, + parseTxsResponseUnsigned, + parseUnsignedTx, } from "./decode"; -import { chainId, nonce, signedTxJson, txId } from "./testdata.spec"; -import data from "./testdata/cosmoshub.json"; +import * as testdata from "./testdata.spec"; +import cosmoshub from "./testdata/cosmoshub.json"; import { BankTokens } from "./types"; const { fromBase64, fromHex } = Encoding; @@ -28,7 +30,7 @@ describe("decode", () => { "1nUcIH0CLT0/nQ0mBTDrT6kMG20NY/PsH7P2gc4bpYNGLEYjBmdWevXUJouSE/9A/60QG9cYeqyTe5kFDeIPxQ==", ); const defaultFullSignature = { - nonce: nonce, + nonce: testdata.nonce, pubkey: defaultPubkey, signature: defaultSignature, }; @@ -37,12 +39,14 @@ describe("decode", () => { quantity: "11657995", tokenTicker: "ATOM" as TokenTicker, }; - const defaultSendTransaction = { - kind: "bcp/send" as const, - chainId: chainId, + 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: { @@ -107,7 +111,7 @@ describe("decode", () => { }, signature: "1nUcIH0CLT0/nQ0mBTDrT6kMG20NY/PsH7P2gc4bpYNGLEYjBmdWevXUJouSE/9A/60QG9cYeqyTe5kFDeIPxQ==", }; - expect(decodeFullSignature(fullSignature, nonce)).toEqual(defaultFullSignature); + expect(decodeFullSignature(fullSignature, testdata.nonce)).toEqual(defaultFullSignature); }); }); @@ -136,7 +140,7 @@ describe("decode", () => { ], }, }; - expect(parseMsg(msg, chainId, defaultTokens)).toEqual(defaultSendTransaction); + expect(parseMsg(msg, defaultMemo, testdata.chainId, defaultTokens)).toEqual(defaultSendTransaction); }); }); @@ -155,29 +159,63 @@ describe("decode", () => { }); }); - describe("parseTx", () => { + describe("parseUnsignedTx", () => { it("works", () => { - expect(parseTx(data.tx.value, chainId, nonce, defaultTokens)).toEqual(signedTxJson); + expect(parseUnsignedTx(cosmoshub.tx.value, testdata.chainId, defaultTokens)).toEqual( + testdata.sendTxJson, + ); }); }); - describe("parseTxsResponse", () => { + 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 = { height: "2823", - txhash: txId, + txhash: testdata.txId, raw_log: '[{"msg_index":0,"success":true,"log":""}]', - tx: data.tx, + tx: cosmoshub.tx, }; const expected = { - ...signedTxJson, + transaction: testdata.sendTxJson, height: 2823, confirmations: 101, - transactionId: txId, + transactionId: testdata.txId, log: '[{"msg_index":0,"success":true,"log":""}]', }; - expect(parseTxsResponse(chainId, currentHeight, nonce, txsResponse, defaultTokens)).toEqual(expected); + expect(parseTxsResponseUnsigned(testdata.chainId, currentHeight, txsResponse, defaultTokens)).toEqual( + expected, + ); + }); + }); + + describe("parseTxsResponseSigned", () => { + it("works", () => { + const currentHeight = 2923; + const txsResponse = { + height: "2823", + txhash: testdata.txId, + raw_log: '[{"msg_index":0,"success":true,"log":""}]', + tx: cosmoshub.tx, + }; + 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); }); }); }); diff --git a/packages/bcp/src/decode.ts b/packages/bcp/src/decode.ts index bd4a115d..b57eb8bd 100644 --- a/packages/bcp/src/decode.ts +++ b/packages/bcp/src/decode.ts @@ -5,6 +5,7 @@ import { Amount, ChainId, ConfirmedAndSignedTransaction, + ConfirmedTransaction, Fee, FullSignature, Nonce, @@ -70,7 +71,12 @@ export function decodeAmount(tokens: BankTokens, coin: types.Coin): Amount { }; } -export function parseMsg(msg: types.Msg, chainId: ChainId, tokens: BankTokens): UnsignedTransaction { +export function parseMsg( + msg: types.Msg, + memo: string | undefined, + chainId: ChainId, + tokens: BankTokens, +): UnsignedTransaction { if (types.isMsgSend(msg)) { if (msg.value.amount.length !== 1) { throw new Error("Only MsgSend with one amount is supported"); @@ -81,6 +87,7 @@ export function parseMsg(msg: types.Msg, chainId: ChainId, tokens: BankTokens): 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 { @@ -103,12 +110,11 @@ export function parseFee(fee: types.StdFee, tokens: BankTokens): Fee { }; } -export function parseTx( +export function parseUnsignedTx( txValue: types.StdTx, chainId: ChainId, - nonce: Nonce, tokens: BankTokens, -): SignedTransaction { +): UnsignedTransaction { if (!types.isStdTx(txValue)) { throw new Error("Only StdTx is supported"); } @@ -116,24 +122,46 @@ export function parseTx( throw new Error("Only single-message transactions currently supported"); } - const [primarySignature] = txValue.signatures.map(signature => decodeFullSignature(signature, nonce)); - const msg = parseMsg(txValue.msg[0], chainId, tokens); + const msg = parseMsg(txValue.msg[0], txValue.memo, chainId, tokens); const fee = parseFee(txValue.fee, tokens); - const transaction = { + return { ...msg, chainId: chainId, - memo: txValue.memo, fee: fee, }; +} +export function parseSignedTx( + txValue: types.StdTx, + chainId: ChainId, + nonce: Nonce, + tokens: BankTokens, +): SignedTransaction { + const [primarySignature] = txValue.signatures.map(signature => decodeFullSignature(signature, nonce)); return { - transaction: transaction, + transaction: parseUnsignedTx(txValue, chainId, tokens), signatures: [primarySignature], }; } -export function parseTxsResponse( +export function parseTxsResponseUnsigned( + chainId: ChainId, + currentHeight: number, + response: TxsResponse, + tokens: BankTokens, +): ConfirmedTransaction { + const height = parseInt(response.height, 10); + return { + transaction: parseUnsignedTx(response.tx.value, chainId, tokens), + height: height, + confirmations: currentHeight - height + 1, + transactionId: response.txhash as TransactionId, + log: response.raw_log, + }; +} + +export function parseTxsResponseSigned( chainId: ChainId, currentHeight: number, nonce: Nonce, @@ -142,7 +170,7 @@ export function parseTxsResponse( ): ConfirmedAndSignedTransaction { const height = parseInt(response.height, 10); return { - ...parseTx(response.tx.value, chainId, nonce, tokens), + ...parseSignedTx(response.tx.value, chainId, nonce, tokens), height: height, confirmations: currentHeight - height + 1, transactionId: response.txhash as TransactionId, diff --git a/packages/bcp/types/cosmwasmconnection.d.ts b/packages/bcp/types/cosmwasmconnection.d.ts index e5afc441..7191fbab 100644 --- a/packages/bcp/types/cosmwasmconnection.d.ts +++ b/packages/bcp/types/cosmwasmconnection.d.ts @@ -85,5 +85,6 @@ export declare class CosmWasmConnection implements BlockchainConnection { liveTx(_query: TransactionQuery): Stream | FailedTransaction>; getFeeQuote(tx: UnsignedTransaction): Promise; withDefaultFee(tx: T): Promise; - private parseAndPopulateTxResponse; + private parseAndPopulateTxResponseUnsigned; + private parseAndPopulateTxResponseSigned; } diff --git a/packages/bcp/types/decode.d.ts b/packages/bcp/types/decode.d.ts index 4ef7d075..c895d2a0 100644 --- a/packages/bcp/types/decode.d.ts +++ b/packages/bcp/types/decode.d.ts @@ -3,6 +3,7 @@ import { Amount, ChainId, ConfirmedAndSignedTransaction, + ConfirmedTransaction, Fee, FullSignature, Nonce, @@ -18,15 +19,31 @@ export declare function decodeSignature(signature: string): SignatureBytes; export declare function decodeFullSignature(signature: types.StdSignature, nonce: number): FullSignature; export declare function coinToDecimal(tokens: BankTokens, coin: types.Coin): readonly [Decimal, string]; export declare function decodeAmount(tokens: BankTokens, coin: types.Coin): Amount; -export declare function parseMsg(msg: types.Msg, chainId: ChainId, tokens: BankTokens): UnsignedTransaction; +export declare function parseMsg( + msg: types.Msg, + memo: string | undefined, + chainId: ChainId, + tokens: BankTokens, +): UnsignedTransaction; export declare function parseFee(fee: types.StdFee, tokens: BankTokens): Fee; -export declare function parseTx( +export declare function parseUnsignedTx( + txValue: types.StdTx, + chainId: ChainId, + tokens: BankTokens, +): UnsignedTransaction; +export declare function parseSignedTx( txValue: types.StdTx, chainId: ChainId, nonce: Nonce, tokens: BankTokens, ): SignedTransaction; -export declare function parseTxsResponse( +export declare function parseTxsResponseUnsigned( + chainId: ChainId, + currentHeight: number, + response: TxsResponse, + tokens: BankTokens, +): ConfirmedTransaction; +export declare function parseTxsResponseSigned( chainId: ChainId, currentHeight: number, nonce: Nonce,