From a2068f79de3858ae5e98d69e5cc928ab01c58ad1 Mon Sep 17 00:00:00 2001 From: Simon Warta Date: Fri, 14 Feb 2020 12:38:33 +0100 Subject: [PATCH 1/8] Move ignore to top of file --- packages/sdk/src/cosmwasmclient.spec.ts | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/packages/sdk/src/cosmwasmclient.spec.ts b/packages/sdk/src/cosmwasmclient.spec.ts index cab410d3..3feab7d3 100644 --- a/packages/sdk/src/cosmwasmclient.spec.ts +++ b/packages/sdk/src/cosmwasmclient.spec.ts @@ -1,3 +1,4 @@ +/* eslint-disable @typescript-eslint/camelcase */ import { Bech32, Encoding } from "@iov/encoding"; import { assert } from "@iov/utils"; @@ -91,9 +92,7 @@ describe("CosmWasmClient", () => { const sendMsg: MsgSend = { type: "cosmos-sdk/MsgSend", value: { - // eslint-disable-next-line @typescript-eslint/camelcase from_address: faucet.address, - // eslint-disable-next-line @typescript-eslint/camelcase to_address: makeRandomAddress(), amount: [ { @@ -152,9 +151,7 @@ describe("CosmWasmClient", () => { const sendMsg: MsgSend = { type: "cosmos-sdk/MsgSend", value: { - // eslint-disable-next-line @typescript-eslint/camelcase from_address: faucet.address, - // eslint-disable-next-line @typescript-eslint/camelcase to_address: makeRandomAddress(), amount: [ { From 0818542fecfae7858b7e90de1e6593909d9869e7 Mon Sep 17 00:00:00 2001 From: Simon Warta Date: Fri, 14 Feb 2020 13:43:18 +0100 Subject: [PATCH 2/8] Add some fields to TxsResponse --- packages/bcp/src/decode.spec.ts | 2 ++ packages/sdk/src/restclient.ts | 6 ++++++ packages/sdk/types/restclient.d.ts | 6 ++++++ 3 files changed, 14 insertions(+) diff --git a/packages/bcp/src/decode.spec.ts b/packages/bcp/src/decode.spec.ts index ce04f912..dde064c6 100644 --- a/packages/bcp/src/decode.spec.ts +++ b/packages/bcp/src/decode.spec.ts @@ -183,6 +183,7 @@ describe("decode", () => { txhash: testdata.txId, raw_log: '[{"msg_index":0,"success":true,"log":""}]', tx: cosmoshub.tx, + timestamp: "2020-02-14T11:35:41Z", }; const expected = { transaction: testdata.sendTxJson, @@ -205,6 +206,7 @@ describe("decode", () => { txhash: testdata.txId, raw_log: '[{"msg_index":0,"success":true,"log":""}]', tx: cosmoshub.tx, + timestamp: "2020-02-14T11:35:41Z", }; const expected = { ...testdata.signedTxJson, diff --git a/packages/sdk/src/restclient.ts b/packages/sdk/src/restclient.ts index 3fd3c847..2126cb7c 100644 --- a/packages/sdk/src/restclient.ts +++ b/packages/sdk/src/restclient.ts @@ -69,7 +69,13 @@ export interface TxsResponse { readonly height: string; readonly txhash: string; readonly raw_log: string; + readonly logs?: object; readonly tx: CosmosSdkTx; + /** The gas limit as set by the user */ + readonly gas_wanted?: string; + /** The gas used by the execution */ + readonly gas_used?: string; + readonly timestamp: string; } interface SearchTxsResponse { diff --git a/packages/sdk/types/restclient.d.ts b/packages/sdk/types/restclient.d.ts index 2da34281..8b82fb29 100644 --- a/packages/sdk/types/restclient.d.ts +++ b/packages/sdk/types/restclient.d.ts @@ -42,7 +42,13 @@ export interface TxsResponse { readonly height: string; readonly txhash: string; readonly raw_log: string; + readonly logs?: object; readonly tx: CosmosSdkTx; + /** The gas limit as set by the user */ + readonly gas_wanted?: string; + /** The gas used by the execution */ + readonly gas_used?: string; + readonly timestamp: string; } interface SearchTxsResponse { readonly total_count: string; From 83479f3bf714bffffe180b732ac60d9b6627af4a Mon Sep 17 00:00:00 2001 From: Simon Warta Date: Fri, 14 Feb 2020 14:06:03 +0100 Subject: [PATCH 3/8] Add findSequenceForSignedTx --- packages/sdk/src/index.ts | 1 + packages/sdk/src/sequence.spec.ts | 30 ++++++++++++ packages/sdk/src/sequence.ts | 46 +++++++++++++++++ packages/sdk/src/testdata/txresponse1.json | 57 ++++++++++++++++++++++ packages/sdk/src/testdata/txresponse2.json | 57 ++++++++++++++++++++++ packages/sdk/src/testdata/txresponse3.json | 57 ++++++++++++++++++++++ packages/sdk/types/index.d.ts | 1 + packages/sdk/types/sequence.d.ts | 19 ++++++++ 8 files changed, 268 insertions(+) create mode 100644 packages/sdk/src/sequence.spec.ts create mode 100644 packages/sdk/src/sequence.ts create mode 100644 packages/sdk/src/testdata/txresponse1.json create mode 100644 packages/sdk/src/testdata/txresponse2.json create mode 100644 packages/sdk/src/testdata/txresponse3.json create mode 100644 packages/sdk/types/sequence.d.ts diff --git a/packages/sdk/src/index.ts b/packages/sdk/src/index.ts index e144a09e..4352341b 100644 --- a/packages/sdk/src/index.ts +++ b/packages/sdk/src/index.ts @@ -24,3 +24,4 @@ export { encodeBech32Pubkey, encodeSecp256k1Pubkey, } from "./pubkey"; +export { findSequenceForSignedTx } from "./sequence"; diff --git a/packages/sdk/src/sequence.spec.ts b/packages/sdk/src/sequence.spec.ts new file mode 100644 index 00000000..e5d8f4cd --- /dev/null +++ b/packages/sdk/src/sequence.spec.ts @@ -0,0 +1,30 @@ +import { findSequenceForSignedTx } from "./sequence"; +import response1 from "./testdata/txresponse1.json"; +import response2 from "./testdata/txresponse2.json"; +import response3 from "./testdata/txresponse3.json"; + +// Those values must match ./testdata/txresponse*.json +const chainId = "testing"; +const accountNumber = 4; + +describe("sequence", () => { + describe("findSequenceForSignedTx", () => { + it("works", async () => { + const current = 100; // what we get from GET /auth/accounts/{address} + expect(await findSequenceForSignedTx(response1.tx, chainId, accountNumber, current)).toEqual(10); + // We know response3.height > response1.height, so the sequence must be at least 10+1 + expect(await findSequenceForSignedTx(response3.tx, chainId, accountNumber, current, 11)).toEqual(19); + // We know response3.height > response2.height > response1.height, so the sequence must be at least 10+1 and smaller than 19 + expect(await findSequenceForSignedTx(response2.tx, chainId, accountNumber, 19, 11)).toEqual(13); + }); + + it("returns undefined when sequence is not in range", async () => { + expect(await findSequenceForSignedTx(response1.tx, chainId, accountNumber, 5)).toBeUndefined(); + expect(await findSequenceForSignedTx(response1.tx, chainId, accountNumber, 20, 11)).toBeUndefined(); + expect(await findSequenceForSignedTx(response1.tx, chainId, accountNumber, 20, 50)).toBeUndefined(); + + // upper bound is not included in the possible results + expect(await findSequenceForSignedTx(response1.tx, chainId, accountNumber, 10)).toBeUndefined(); + }); + }); +}); diff --git a/packages/sdk/src/sequence.ts b/packages/sdk/src/sequence.ts new file mode 100644 index 00000000..8a53e897 --- /dev/null +++ b/packages/sdk/src/sequence.ts @@ -0,0 +1,46 @@ +import { Secp256k1, Secp256k1Signature, Sha256 } from "@iov/crypto"; + +import { makeSignBytes } from "./encoding"; +import { decodeSignature } from "./signature"; +import { CosmosSdkTx } from "./types"; + +/** + * Serach for sequence s with `min` <= `s` < `upperBound` to find the sequence that was used to sign the transaction + * + * @param tx The signed transaction + * @param chainId The chain ID for which this transaction was signed + * @param accountNumber The account number for which this transaction was signed + * @param upperBound The upper bound for the testing, i.e. sequence must be lower than this value + * @param min The lowest sequence that is tested + * + * @returns the sequence if a match was found and undefined otherwise + */ +export async function findSequenceForSignedTx( + tx: CosmosSdkTx, + chainId: string, + accountNumber: number, + upperBound: number, + min = 1, +): Promise { + const firstSignature = tx.value.signatures.find(() => true); + if (!firstSignature) throw new Error("Signature missing in tx"); + + const { pubkey, signature } = decodeSignature(firstSignature); + const secp256keSignature = new Secp256k1Signature(signature.slice(0, 32), signature.slice(32, 64)); + + for (let s = min; s < upperBound; s++) { + // console.log(`Trying sequence ${s}`); + const signBytes = makeSignBytes( + tx.value.msg, + tx.value.fee, + chainId, + tx.value.memo || "", + accountNumber, + s, + ); + const prehashed = new Sha256(signBytes).digest(); + const valid = await Secp256k1.verifySignature(secp256keSignature, prehashed, pubkey); + if (valid) return s; + } + return undefined; +} diff --git a/packages/sdk/src/testdata/txresponse1.json b/packages/sdk/src/testdata/txresponse1.json new file mode 100644 index 00000000..703ea696 --- /dev/null +++ b/packages/sdk/src/testdata/txresponse1.json @@ -0,0 +1,57 @@ +{ + "height": "15888", + "txhash": "672DEDE8EF4DE8B5818959F417CCA357079D4D7A19C4B65443C7FBF8176AABF9", + "raw_log": "[{\"msg_index\":0,\"log\":\"\",\"events\":[{\"type\":\"message\",\"attributes\":[{\"key\":\"action\",\"value\":\"send\"},{\"key\":\"sender\",\"value\":\"cosmos1pkptre7fdkl6gfrzlesjjvhxhlc3r4gmmk8rs6\"},{\"key\":\"module\",\"value\":\"bank\"}]},{\"type\":\"transfer\",\"attributes\":[{\"key\":\"recipient\",\"value\":\"cosmos1t70qnpr0az8tf7py83m4ue5y89w58lkjmx0yq2\"},{\"key\":\"amount\",\"value\":\"75000ucosm\"}]}]}]", + "logs": [ + { + "msg_index": 0, + "log": "", + "events": [ + { + "type": "message", + "attributes": [ + { "key": "action", "value": "send" }, + { "key": "sender", "value": "cosmos1pkptre7fdkl6gfrzlesjjvhxhlc3r4gmmk8rs6" }, + { "key": "module", "value": "bank" } + ] + }, + { + "type": "transfer", + "attributes": [ + { "key": "recipient", "value": "cosmos1t70qnpr0az8tf7py83m4ue5y89w58lkjmx0yq2" }, + { "key": "amount", "value": "75000ucosm" } + ] + } + ] + } + ], + "gas_wanted": "200000", + "gas_used": "65407", + "tx": { + "type": "cosmos-sdk/StdTx", + "value": { + "msg": [ + { + "type": "cosmos-sdk/MsgSend", + "value": { + "from_address": "cosmos1pkptre7fdkl6gfrzlesjjvhxhlc3r4gmmk8rs6", + "to_address": "cosmos1t70qnpr0az8tf7py83m4ue5y89w58lkjmx0yq2", + "amount": [{ "denom": "ucosm", "amount": "75000" }] + } + } + ], + "fee": { "amount": [{ "denom": "ucosm", "amount": "5000" }], "gas": "200000" }, + "signatures": [ + { + "pub_key": { + "type": "tendermint/PubKeySecp256k1", + "value": "A08EGB7ro1ORuFhjOnZcSgwYlpe0DSFjVNUIkNNQxwKQ" + }, + "signature": "US7oH8S/8TxVrtBQkOhHxAM+oDB2spNAEawgh6H8CCFLRMOJK+uvQZZ6ceUgUsvDbxwCz7re1RU272fymMYRZQ==" + } + ], + "memo": "My first payment" + } + }, + "timestamp": "2020-02-14T11:25:55Z" +} diff --git a/packages/sdk/src/testdata/txresponse2.json b/packages/sdk/src/testdata/txresponse2.json new file mode 100644 index 00000000..0d1a37f9 --- /dev/null +++ b/packages/sdk/src/testdata/txresponse2.json @@ -0,0 +1,57 @@ +{ + "height": "16456", + "txhash": "7BFE4B93AF190F60132C62D08FDF50BE462FBCE374EB13D3FD0C32461E771EC0", + "raw_log": "[{\"msg_index\":0,\"log\":\"\",\"events\":[{\"type\":\"message\",\"attributes\":[{\"key\":\"action\",\"value\":\"send\"},{\"key\":\"sender\",\"value\":\"cosmos1pkptre7fdkl6gfrzlesjjvhxhlc3r4gmmk8rs6\"},{\"key\":\"module\",\"value\":\"bank\"}]},{\"type\":\"transfer\",\"attributes\":[{\"key\":\"recipient\",\"value\":\"cosmos1t70qnpr0az8tf7py83m4ue5y89w58lkjmx0yq2\"},{\"key\":\"amount\",\"value\":\"75000ucosm\"}]}]}]", + "logs": [ + { + "msg_index": 0, + "log": "", + "events": [ + { + "type": "message", + "attributes": [ + { "key": "action", "value": "send" }, + { "key": "sender", "value": "cosmos1pkptre7fdkl6gfrzlesjjvhxhlc3r4gmmk8rs6" }, + { "key": "module", "value": "bank" } + ] + }, + { + "type": "transfer", + "attributes": [ + { "key": "recipient", "value": "cosmos1t70qnpr0az8tf7py83m4ue5y89w58lkjmx0yq2" }, + { "key": "amount", "value": "75000ucosm" } + ] + } + ] + } + ], + "gas_wanted": "200000", + "gas_used": "65407", + "tx": { + "type": "cosmos-sdk/StdTx", + "value": { + "msg": [ + { + "type": "cosmos-sdk/MsgSend", + "value": { + "from_address": "cosmos1pkptre7fdkl6gfrzlesjjvhxhlc3r4gmmk8rs6", + "to_address": "cosmos1t70qnpr0az8tf7py83m4ue5y89w58lkjmx0yq2", + "amount": [{ "denom": "ucosm", "amount": "75000" }] + } + } + ], + "fee": { "amount": [{ "denom": "ucosm", "amount": "5000" }], "gas": "200000" }, + "signatures": [ + { + "pub_key": { + "type": "tendermint/PubKeySecp256k1", + "value": "A08EGB7ro1ORuFhjOnZcSgwYlpe0DSFjVNUIkNNQxwKQ" + }, + "signature": "ltvd9Rb3RF4zjbUVrpDpkok34g+py7XR8ZcM0tZUYRxxVdcMEin010x+ZFd/mOuutPj9fDmSENnienc/yi4msw==" + } + ], + "memo": "My first payment" + } + }, + "timestamp": "2020-02-14T11:35:41Z" +} diff --git a/packages/sdk/src/testdata/txresponse3.json b/packages/sdk/src/testdata/txresponse3.json new file mode 100644 index 00000000..8ffd0727 --- /dev/null +++ b/packages/sdk/src/testdata/txresponse3.json @@ -0,0 +1,57 @@ +{ + "height": "20730", + "txhash": "625BC75E697F73DA037387C34002BB2F682E7ACDCC4E015D3E90420516C6D0C8", + "raw_log": "[{\"msg_index\":0,\"log\":\"\",\"events\":[{\"type\":\"message\",\"attributes\":[{\"key\":\"action\",\"value\":\"send\"},{\"key\":\"sender\",\"value\":\"cosmos1pkptre7fdkl6gfrzlesjjvhxhlc3r4gmmk8rs6\"},{\"key\":\"module\",\"value\":\"bank\"}]},{\"type\":\"transfer\",\"attributes\":[{\"key\":\"recipient\",\"value\":\"cosmos1t70qnpr0az8tf7py83m4ue5y89w58lkjmx0yq2\"},{\"key\":\"amount\",\"value\":\"75000ucosm\"}]}]}]", + "logs": [ + { + "msg_index": 0, + "log": "", + "events": [ + { + "type": "message", + "attributes": [ + { "key": "action", "value": "send" }, + { "key": "sender", "value": "cosmos1pkptre7fdkl6gfrzlesjjvhxhlc3r4gmmk8rs6" }, + { "key": "module", "value": "bank" } + ] + }, + { + "type": "transfer", + "attributes": [ + { "key": "recipient", "value": "cosmos1t70qnpr0az8tf7py83m4ue5y89w58lkjmx0yq2" }, + { "key": "amount", "value": "75000ucosm" } + ] + } + ] + } + ], + "gas_wanted": "200000", + "gas_used": "65407", + "tx": { + "type": "cosmos-sdk/StdTx", + "value": { + "msg": [ + { + "type": "cosmos-sdk/MsgSend", + "value": { + "from_address": "cosmos1pkptre7fdkl6gfrzlesjjvhxhlc3r4gmmk8rs6", + "to_address": "cosmos1t70qnpr0az8tf7py83m4ue5y89w58lkjmx0yq2", + "amount": [{ "denom": "ucosm", "amount": "75000" }] + } + } + ], + "fee": { "amount": [{ "denom": "ucosm", "amount": "5000" }], "gas": "200000" }, + "signatures": [ + { + "pub_key": { + "type": "tendermint/PubKeySecp256k1", + "value": "A08EGB7ro1ORuFhjOnZcSgwYlpe0DSFjVNUIkNNQxwKQ" + }, + "signature": "eOFGl1tIHDMv3JdCK9fRSikVbYUD8+B0ksb3dJFya8MPYgpEpdSA7zZc+5n/cW6LR/BJdib4nqmJQv1yD9lm3g==" + } + ], + "memo": "My first payment" + } + }, + "timestamp": "2020-02-14T12:48:56Z" +} diff --git a/packages/sdk/types/index.d.ts b/packages/sdk/types/index.d.ts index 0a4c02d7..dcb618cc 100644 --- a/packages/sdk/types/index.d.ts +++ b/packages/sdk/types/index.d.ts @@ -23,3 +23,4 @@ export { encodeBech32Pubkey, encodeSecp256k1Pubkey, } from "./pubkey"; +export { findSequenceForSignedTx } from "./sequence"; diff --git a/packages/sdk/types/sequence.d.ts b/packages/sdk/types/sequence.d.ts new file mode 100644 index 00000000..70d38469 --- /dev/null +++ b/packages/sdk/types/sequence.d.ts @@ -0,0 +1,19 @@ +import { CosmosSdkTx } from "./types"; +/** + * Serach for sequence s with `min` <= `s` < `upperBound` to find the sequence that was used to sign the transaction + * + * @param tx The signed transaction + * @param chainId The chain ID for which this transaction was signed + * @param accountNumber The account number for which this transaction was signed + * @param upperBound The upper bound for the testing, i.e. sequence must be lower than this value + * @param min The lowest sequence that is tested + * + * @returns the sequence if a match was found and undefined otherwise + */ +export declare function findSequenceForSignedTx( + tx: CosmosSdkTx, + chainId: string, + accountNumber: number, + upperBound: number, + min?: number, +): Promise; From c6683f02190e5a5a33a8d8c70f3582b97775ee7d Mon Sep 17 00:00:00 2001 From: Simon Warta Date: Fri, 14 Feb 2020 15:24:29 +0100 Subject: [PATCH 4/8] Grounp account data into "faucet" object --- packages/bcp/src/cosmwasmconnection.spec.ts | 64 +++++++++++---------- 1 file changed, 33 insertions(+), 31 deletions(-) diff --git a/packages/bcp/src/cosmwasmconnection.spec.ts b/packages/bcp/src/cosmwasmconnection.spec.ts index f626a3ad..03ca2738 100644 --- a/packages/bcp/src/cosmwasmconnection.spec.ts +++ b/packages/bcp/src/cosmwasmconnection.spec.ts @@ -34,22 +34,24 @@ function makeRandomAddress(): Address { return Bech32.encode(defaultPrefix, Random.getBytes(20)) as Address; } +const faucet = { + mnemonic: + "economy stock theory fatal elder harbor betray wasp final emotion task crumble siren bottom lizard educate guess current outdoor pair theory focus wife stone", + path: HdPaths.cosmos(0), + pubkey: { + algo: Algorithm.Secp256k1, + data: fromBase64("A08EGB7ro1ORuFhjOnZcSgwYlpe0DSFjVNUIkNNQxwKQ") as PubkeyBytes, + }, + address: "cosmos1pkptre7fdkl6gfrzlesjjvhxhlc3r4gmmk8rs6" as Address, +}; + describe("CosmWasmConnection", () => { const cosm = "COSM" as TokenTicker; const httpUrl = "http://localhost:1317"; const defaultChainId = "cosmos:testing" as ChainId; const defaultEmptyAddress = "cosmos1h806c7khnvmjlywdrkdgk2vrayy2mmvf9rxk2r" as Address; - const faucetMnemonic = - "economy stock theory fatal elder harbor betray wasp final emotion task crumble siren bottom lizard educate guess current outdoor pair theory focus wife stone"; - const faucetPath = HdPaths.cosmos(0); const defaultRecipient = "cosmos1t70qnpr0az8tf7py83m4ue5y89w58lkjmx0yq2" as Address; - const faucetAccount = { - pubkey: { - algo: Algorithm.Secp256k1, - data: fromBase64("A08EGB7ro1ORuFhjOnZcSgwYlpe0DSFjVNUIkNNQxwKQ") as PubkeyBytes, - }, - address: "cosmos1pkptre7fdkl6gfrzlesjjvhxhlc3r4gmmk8rs6" as Address, - }; + const unusedAccount = { pubkey: { algo: Algorithm.Secp256k1, @@ -256,8 +258,8 @@ describe("CosmWasmConnection", () => { it("has a pubkey when getting account with transactions", async () => { pendingWithoutCosmos(); const connection = await CosmWasmConnection.establish(httpUrl, defaultPrefix, defaultConfig); - const account = await connection.getAccount({ address: faucetAccount.address }); - expect(account?.pubkey).toEqual(faucetAccount.pubkey); + const account = await connection.getAccount({ address: faucet.address }); + expect(account?.pubkey).toEqual(faucet.pubkey); connection.disconnect(); }); }); @@ -282,14 +284,14 @@ describe("CosmWasmConnection", () => { pendingWithoutCosmos(); const connection = await CosmWasmConnection.establish(httpUrl, defaultPrefix, defaultConfig); const profile = new UserProfile(); - const wallet = profile.addWallet(Secp256k1HdWallet.fromMnemonic(faucetMnemonic)); - const faucet = await profile.createIdentity(wallet.id, defaultChainId, faucetPath); - const faucetAddress = connection.codec.identityToAddress(faucet); + const wallet = profile.addWallet(Secp256k1HdWallet.fromMnemonic(faucet.mnemonic)); + const senderIdentity = await profile.createIdentity(wallet.id, defaultChainId, faucet.path); + const senderAddress = connection.codec.identityToAddress(senderIdentity); const unsigned = await connection.withDefaultFee({ kind: "bcp/send", chainId: defaultChainId, - sender: faucetAddress, + sender: senderAddress, recipient: defaultRecipient, memo: "My first payment", amount: { @@ -298,8 +300,8 @@ describe("CosmWasmConnection", () => { tokenTicker: cosm, }, }); - const nonce = await connection.getNonce({ address: faucetAddress }); - const signed = await profile.signTransaction(faucet, unsigned, connection.codec, nonce); + 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; @@ -334,14 +336,14 @@ describe("CosmWasmConnection", () => { pendingWithoutCosmos(); const connection = await CosmWasmConnection.establish(httpUrl, defaultPrefix, defaultConfig); const profile = new UserProfile(); - const wallet = profile.addWallet(Secp256k1HdWallet.fromMnemonic(faucetMnemonic)); - const faucet = await profile.createIdentity(wallet.id, defaultChainId, faucetPath); - const faucetAddress = connection.codec.identityToAddress(faucet); + const wallet = profile.addWallet(Secp256k1HdWallet.fromMnemonic(faucet.mnemonic)); + const sender = await profile.createIdentity(wallet.id, defaultChainId, faucet.path); + const senderAddress = connection.codec.identityToAddress(sender); const unsigned = await connection.withDefaultFee({ kind: "bcp/send", chainId: defaultChainId, - sender: faucetAddress, + sender: senderAddress, recipient: defaultRecipient, memo: "My first payment", amount: { @@ -350,8 +352,8 @@ describe("CosmWasmConnection", () => { tokenTicker: cosm, }, }); - const nonce = await connection.getNonce({ address: faucetAddress }); - const signed = await profile.signTransaction(faucet, unsigned, connection.codec, nonce); + 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; @@ -374,7 +376,7 @@ describe("CosmWasmConnection", () => { expect(byIdTransaction).toEqual(unsigned); // search by sender address - const bySenderResults = await connection.searchTx({ sentFromOrTo: faucetAddress }); + const bySenderResults = await connection.searchTx({ sentFromOrTo: senderAddress }); expect(bySenderResults).toBeTruthy(); expect(bySenderResults.length).toBeGreaterThanOrEqual(1); const bySenderResult = bySenderResults[bySenderResults.length - 1]; @@ -424,15 +426,15 @@ describe("CosmWasmConnection", () => { pendingWithoutCosmos(); const connection = await CosmWasmConnection.establish(httpUrl, defaultPrefix, defaultConfig); const profile = new UserProfile(); - const wallet = profile.addWallet(Secp256k1HdWallet.fromMnemonic(faucetMnemonic)); - const faucet = await profile.createIdentity(wallet.id, defaultChainId, faucetPath); - const faucetAddress = connection.codec.identityToAddress(faucet); + const wallet = profile.addWallet(Secp256k1HdWallet.fromMnemonic(faucet.mnemonic)); + const sender = await profile.createIdentity(wallet.id, defaultChainId, faucet.path); + const senderAddress = connection.codec.identityToAddress(sender); const recipient = makeRandomAddress(); const unsigned = await connection.withDefaultFee({ kind: "bcp/send", chainId: defaultChainId, - sender: faucetAddress, + sender: senderAddress, recipient: recipient, memo: "My first payment", amount: { @@ -441,8 +443,8 @@ describe("CosmWasmConnection", () => { tokenTicker: "BASH" as TokenTicker, }, }); - const nonce = await connection.getNonce({ address: faucetAddress }); - const signed = await profile.signTransaction(faucet, unsigned, connection.codec, nonce); + 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 blockInfo = await response.blockInfo.waitFor(info => !isBlockInfoPending(info)); From e4f12523317a9ff5ea392fb86eefc223aa2fdc8b Mon Sep 17 00:00:00 2001 From: Simon Warta Date: Fri, 14 Feb 2020 15:26:39 +0100 Subject: [PATCH 5/8] Move test into "getTx" block --- packages/bcp/src/cosmwasmconnection.spec.ts | 36 ++++++++++----------- 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/packages/bcp/src/cosmwasmconnection.spec.ts b/packages/bcp/src/cosmwasmconnection.spec.ts index 03ca2738..993201bf 100644 --- a/packages/bcp/src/cosmwasmconnection.spec.ts +++ b/packages/bcp/src/cosmwasmconnection.spec.ts @@ -4,6 +4,7 @@ import { Algorithm, ChainId, isBlockInfoPending, + isBlockInfoSucceeded, isConfirmedTransaction, isSendTransaction, PubkeyBytes, @@ -265,22 +266,7 @@ describe("CosmWasmConnection", () => { }); describe("getTx", () => { - it("throws for non-existent transaction", async () => { - pendingWithoutCosmos(); - const connection = await CosmWasmConnection.establish(httpUrl, defaultPrefix, 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("integration tests", () => { - it("can post and get a transaction", async () => { + it("can get a recently posted transaction", async () => { pendingWithoutCosmos(); const connection = await CosmWasmConnection.establish(httpUrl, defaultPrefix, defaultConfig); const profile = new UserProfile(); @@ -305,8 +291,7 @@ describe("CosmWasmConnection", () => { 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); + await response.blockInfo.waitFor(info => isBlockInfoSucceeded(info)); const getResponse = await connection.getTx(transactionId); expect(getResponse.transactionId).toEqual(transactionId); @@ -332,6 +317,21 @@ describe("CosmWasmConnection", () => { connection.disconnect(); }); + it("throws for non-existent transaction", async () => { + pendingWithoutCosmos(); + const connection = await CosmWasmConnection.establish(httpUrl, defaultPrefix, 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("integration tests", () => { it("can post and search for a transaction", async () => { pendingWithoutCosmos(); const connection = await CosmWasmConnection.establish(httpUrl, defaultPrefix, defaultConfig); From 964965ca8205fac50c54b060c1dcc1154de0a75d Mon Sep 17 00:00:00 2001 From: Simon Warta Date: Fri, 14 Feb 2020 15:55:54 +0100 Subject: [PATCH 6/8] Restore naive implementation for getting sequence --- packages/bcp/src/cosmwasmconnection.spec.ts | 2 +- packages/bcp/src/cosmwasmconnection.ts | 25 ++++++++++++++++----- 2 files changed, 21 insertions(+), 6 deletions(-) diff --git a/packages/bcp/src/cosmwasmconnection.spec.ts b/packages/bcp/src/cosmwasmconnection.spec.ts index 993201bf..3e1c6857 100644 --- a/packages/bcp/src/cosmwasmconnection.spec.ts +++ b/packages/bcp/src/cosmwasmconnection.spec.ts @@ -306,7 +306,7 @@ describe("CosmWasmConnection", () => { expect(transaction).toEqual(unsigned); expect(signatures.length).toEqual(1); expect(signatures[0]).toEqual({ - nonce: -1, // Unfortunately this information is unavailable as previous implementation attempt is broken. See https://github.com/iov-one/iov-core/pull/1390 + nonce: signed.signatures[0].nonce, pubkey: { algo: signed.signatures[0].pubkey.algo, data: Secp256k1.compressPubkey(signed.signatures[0].pubkey.data), diff --git a/packages/bcp/src/cosmwasmconnection.ts b/packages/bcp/src/cosmwasmconnection.ts index 2dcf49a4..3f1c21ce 100644 --- a/packages/bcp/src/cosmwasmconnection.ts +++ b/packages/bcp/src/cosmwasmconnection.ts @@ -1,5 +1,5 @@ /* eslint-disable @typescript-eslint/camelcase */ -import { CosmosAddressBech32Prefix, CosmWasmClient, RestClient, TxsResponse } from "@cosmwasm/sdk"; +import { CosmosAddressBech32Prefix, CosmWasmClient, RestClient, TxsResponse, types } from "@cosmwasm/sdk"; import { Account, AccountQuery, @@ -359,10 +359,25 @@ export class CosmWasmConnection implements BlockchainConnection { private async parseAndPopulateTxResponseSigned( response: TxsResponse, ): Promise | FailedTransaction> { - // There is no known way to get the nonce that was used for signing a transaction. - // This information is nesessary for signature validation. - // TODO: fix - const nonce = -1 as Nonce; + 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 (types.isMsgSend(firstMsg)) { + senderAddress = firstMsg.value.from_address; + } else if ( + types.isMsgStoreCode(firstMsg) || + types.isMsgInstantiateContract(firstMsg) || + types.isMsgExecuteContract(firstMsg) + ) { + senderAddress = firstMsg.value.sender; + } else { + throw new Error(`Got unsupported type of message: ${firstMsg.type}`); + } + + const { accountNumber, sequence: currentSequence } = await this.cosmWasmClient.getNonce(senderAddress); + + const nonce = accountToNonce(accountNumber, currentSequence - 1); return parseTxsResponseSigned( this.chainId, From b15dc96ea60b262d14a74294b4c45b16b42a1e44 Mon Sep 17 00:00:00 2001 From: Simon Warta Date: Fri, 14 Feb 2020 16:06:05 +0100 Subject: [PATCH 7/8] Test and use findSequenceForSignedTx --- packages/bcp/src/cosmwasmconnection.spec.ts | 46 ++++++++++++++++++++- packages/bcp/src/cosmwasmconnection.ts | 18 +++++++- packages/sdk/src/index.ts | 2 +- packages/sdk/types/index.d.ts | 2 +- 4 files changed, 62 insertions(+), 6 deletions(-) diff --git a/packages/bcp/src/cosmwasmconnection.spec.ts b/packages/bcp/src/cosmwasmconnection.spec.ts index 3e1c6857..a886e0b7 100644 --- a/packages/bcp/src/cosmwasmconnection.spec.ts +++ b/packages/bcp/src/cosmwasmconnection.spec.ts @@ -1,4 +1,4 @@ -import { CosmosAddressBech32Prefix } from "@cosmwasm/sdk"; +import { CosmosAddressBech32Prefix, decodeSignature } from "@cosmwasm/sdk"; import { Address, Algorithm, @@ -13,12 +13,13 @@ import { TransactionId, TransactionState, } from "@iov/bcp"; -import { Random, Secp256k1 } from "@iov/crypto"; +import { Random, Secp256k1, Secp256k1Signature, Sha256 } from "@iov/crypto"; import { Bech32, Encoding } from "@iov/encoding"; import { HdPaths, Secp256k1HdWallet, UserProfile } from "@iov/keycontrol"; import { assert } from "@iov/utils"; import { CosmWasmConnection, TokenConfiguration } from "./cosmwasmconnection"; +import { encodeFullSignature } from "./encode"; import * as testdata from "./testdata.spec"; const { fromBase64 } = Encoding; @@ -317,6 +318,47 @@ describe("CosmWasmConnection", () => { connection.disconnect(); }); + it("can get an old transaction", async () => { + pendingWithoutCosmos(); + const connection = await CosmWasmConnection.establish(httpUrl, defaultPrefix, defaultConfig); + + const results = await connection.searchTx({ sentFromOrTo: faucet.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( + new Secp256k1Signature(signature.slice(0, 32), signature.slice(32, 64)), + prehashed, + pubkey, + ); + expect(valid).toEqual(true); + + connection.disconnect(); + }); + it("throws for non-existent transaction", async () => { pendingWithoutCosmos(); const connection = await CosmWasmConnection.establish(httpUrl, defaultPrefix, defaultConfig); diff --git a/packages/bcp/src/cosmwasmconnection.ts b/packages/bcp/src/cosmwasmconnection.ts index 3f1c21ce..ffe4540a 100644 --- a/packages/bcp/src/cosmwasmconnection.ts +++ b/packages/bcp/src/cosmwasmconnection.ts @@ -1,5 +1,12 @@ /* eslint-disable @typescript-eslint/camelcase */ -import { CosmosAddressBech32Prefix, CosmWasmClient, RestClient, TxsResponse, types } from "@cosmwasm/sdk"; +import { + CosmosAddressBech32Prefix, + CosmWasmClient, + findSequenceForSignedTx, + RestClient, + TxsResponse, + types, +} from "@cosmwasm/sdk"; import { Account, AccountQuery, @@ -376,8 +383,15 @@ export class CosmWasmConnection implements BlockchainConnection { } const { accountNumber, sequence: currentSequence } = await this.cosmWasmClient.getNonce(senderAddress); + const sequenceForTx = await findSequenceForSignedTx( + response.tx, + Caip5.decode(this.chainId), + accountNumber, + currentSequence, + ); + if (!sequenceForTx) throw new Error("Cound not find matching sequence for this transaction"); - const nonce = accountToNonce(accountNumber, currentSequence - 1); + const nonce = accountToNonce(accountNumber, sequenceForTx); return parseTxsResponseSigned( this.chainId, diff --git a/packages/sdk/src/index.ts b/packages/sdk/src/index.ts index 4352341b..4483ad8d 100644 --- a/packages/sdk/src/index.ts +++ b/packages/sdk/src/index.ts @@ -6,7 +6,6 @@ export { CosmosAddressBech32Prefix, encodeAddress, isValidAddress } from "./addr export { unmarshalTx } from "./decoding"; export { makeSignBytes, marshalTx } from "./encoding"; export { RestClient, TxsResponse } from "./restclient"; -export { encodeSecp256k1Signature } from "./signature"; export { CosmWasmClient, ExecuteResult, @@ -25,3 +24,4 @@ export { encodeSecp256k1Pubkey, } from "./pubkey"; export { findSequenceForSignedTx } from "./sequence"; +export { encodeSecp256k1Signature, decodeSignature } from "./signature"; diff --git a/packages/sdk/types/index.d.ts b/packages/sdk/types/index.d.ts index dcb618cc..18dd10d2 100644 --- a/packages/sdk/types/index.d.ts +++ b/packages/sdk/types/index.d.ts @@ -5,7 +5,6 @@ export { CosmosAddressBech32Prefix, encodeAddress, isValidAddress } from "./addr export { unmarshalTx } from "./decoding"; export { makeSignBytes, marshalTx } from "./encoding"; export { RestClient, TxsResponse } from "./restclient"; -export { encodeSecp256k1Signature } from "./signature"; export { CosmWasmClient, ExecuteResult, @@ -24,3 +23,4 @@ export { encodeSecp256k1Pubkey, } from "./pubkey"; export { findSequenceForSignedTx } from "./sequence"; +export { encodeSecp256k1Signature, decodeSignature } from "./signature"; From 4f56be8c3428b71179ab37a300670e81dba78dfe Mon Sep 17 00:00:00 2001 From: Simon Warta Date: Fri, 14 Feb 2020 23:37:41 +0100 Subject: [PATCH 8/8] Stabilize beforeAll() of searchTx --- packages/sdk/src/cosmwasmclient.spec.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/sdk/src/cosmwasmclient.spec.ts b/packages/sdk/src/cosmwasmclient.spec.ts index 3feab7d3..ce821e7c 100644 --- a/packages/sdk/src/cosmwasmclient.spec.ts +++ b/packages/sdk/src/cosmwasmclient.spec.ts @@ -1,6 +1,6 @@ /* eslint-disable @typescript-eslint/camelcase */ import { Bech32, Encoding } from "@iov/encoding"; -import { assert } from "@iov/utils"; +import { assert, sleep } from "@iov/utils"; import { CosmWasmClient } from "./cosmwasmclient"; import { makeSignBytes, marshalTx } from "./encoding"; @@ -143,7 +143,6 @@ describe("CosmWasmClient", () => { beforeAll(async () => { if (cosmosEnabled()) { - pendingWithoutCosmos(); const pen = await Secp256k1Pen.fromMnemonic(faucet.mnemonic); const client = CosmWasmClient.makeReadOnly(httpUrl); @@ -184,6 +183,7 @@ describe("CosmWasmClient", () => { }; const result = await client.postTx(marshalTx(signedTx)); + await sleep(50); // wait until tx is indexed const txDetails = await new RestClient(httpUrl).txsById(result.transactionHash); posted = { sender: sendMsg.value.from_address,