diff --git a/packages/bcp/src/cosmwasmconnection.spec.ts b/packages/bcp/src/cosmwasmconnection.spec.ts index 4355fd57..864f41e5 100644 --- a/packages/bcp/src/cosmwasmconnection.spec.ts +++ b/packages/bcp/src/cosmwasmconnection.spec.ts @@ -589,6 +589,124 @@ describe("CosmWasmConnection", () => { connection.disconnect(); }); + it("can post an ERC20 transfer and search for the transaction", async () => { + pendingWithoutWasmd(); + const connection = await CosmWasmConnection.establish(httpUrl, defaultAddressPrefix, defaultConfig); + const profile = new UserProfile(); + 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: senderAddress, + recipient: recipient, + memo: "My first payment", + amount: { + quantity: "75", + fractionalDigits: 0, + tokenTicker: "ISA" as TokenTicker, + }, + }); + const nonce = await connection.getNonce({ address: senderAddress }); + const signed = await profile.signTransaction(sender, unsigned, connection.codec, nonce); + const postableBytes = connection.codec.bytesToPost(signed); + const response = await connection.postTx(postableBytes); + const { transactionId } = response; + const blockInfo = await response.blockInfo.waitFor(info => !isBlockInfoPending(info)); + expect(blockInfo.state).toEqual(TransactionState.Succeeded); + + // search by id + const byIdResults = await connection.searchTx({ id: transactionId }); + expect(byIdResults.length).toEqual(1); + const byIdResult = byIdResults[0]; + expect(byIdResult.transactionId).toEqual(transactionId); + assert(isConfirmedTransaction(byIdResult), "Expected transaction to succeed"); + assert(byIdResult.log, "Log must be available"); + const [firstByIdlog] = JSON.parse(byIdResult.log); + expect(firstByIdlog.events.length).toEqual(2); + expect(firstByIdlog.events[0].type).toEqual("message"); + expect(firstByIdlog.events[1].type).toEqual("wasm"); + // wasm event attributes added by contract + expect(firstByIdlog.events[1].attributes).toContain({ key: "action", value: "transfer" }); + expect(firstByIdlog.events[1].attributes).toContain({ key: "sender", value: senderAddress }); + expect(firstByIdlog.events[1].attributes).toContain({ key: "recipient", value: recipient }); + // wasm event attributes added wasmd + expect(firstByIdlog.events[1].attributes).toContain({ + key: "contract_address", + value: defaultConfig.erc20Tokens![1].contractAddress, + }); + const byIdTransaction = byIdResult.transaction; + assert(isSendTransaction(byIdTransaction), "Expected send transaction"); + expect(byIdTransaction).toEqual(unsigned); + + // search by sender address + const bySenderResults = await connection.searchTx({ sentFromOrTo: senderAddress }); + expect(bySenderResults).toBeTruthy(); + expect(bySenderResults.length).toBeGreaterThanOrEqual(1); + const bySenderResult = bySenderResults[bySenderResults.length - 1]; + expect(bySenderResult.transactionId).toEqual(transactionId); + assert(isConfirmedTransaction(bySenderResult), "Expected transaction to succeed"); + assert(bySenderResult.log, "Log must be available"); + const [firstBySenderLog] = JSON.parse(bySenderResult.log); + expect(firstBySenderLog.events.length).toEqual(2); + expect(firstBySenderLog.events[0].type).toEqual("message"); + expect(firstBySenderLog.events[1].type).toEqual("wasm"); + // wasm event attributes added by contract + expect(firstBySenderLog.events[1].attributes).toContain({ key: "action", value: "transfer" }); + expect(firstBySenderLog.events[1].attributes).toContain({ key: "sender", value: senderAddress }); + expect(firstBySenderLog.events[1].attributes).toContain({ key: "recipient", value: recipient }); + // wasm event attributes added wasmd + expect(firstBySenderLog.events[1].attributes).toContain({ + key: "contract_address", + value: defaultConfig.erc20Tokens![1].contractAddress, + }); + const bySenderTransaction = bySenderResult.transaction; + assert(isSendTransaction(bySenderTransaction), "Expected send transaction"); + expect(bySenderTransaction).toEqual(unsigned); + + // search by recipient address + const byRecipientResults = await connection.searchTx({ sentFromOrTo: recipient }); + expect(byRecipientResults.length).toBeGreaterThanOrEqual(1); + const byRecipientResult = byRecipientResults[byRecipientResults.length - 1]; + expect(byRecipientResult.transactionId).toEqual(transactionId); + assert(isConfirmedTransaction(byRecipientResult), "Expected transaction to succeed"); + assert(byRecipientResult.log, "Log must be available"); + const [firstByRecipientLog] = JSON.parse(bySenderResult.log); + expect(firstByRecipientLog.events.length).toEqual(2); + expect(firstByRecipientLog.events[0].type).toEqual("message"); + expect(firstByRecipientLog.events[1].type).toEqual("wasm"); + // wasm event attributes added by contract + expect(firstByRecipientLog.events[1].attributes).toContain({ key: "action", value: "transfer" }); + expect(firstByRecipientLog.events[1].attributes).toContain({ key: "sender", value: senderAddress }); + expect(firstByRecipientLog.events[1].attributes).toContain({ key: "recipient", value: recipient }); + // wasm event attributes added wasmd + expect(firstByRecipientLog.events[1].attributes).toContain({ + key: "contract_address", + value: defaultConfig.erc20Tokens![1].contractAddress, + }); + const byRecipeintTransaction = byRecipientResult.transaction; + assert(isSendTransaction(byRecipeintTransaction), "Expected send transaction"); + expect(byRecipeintTransaction).toEqual(unsigned); + + // search by height + const heightResults = await connection.searchTx({ height: byIdResult.height }); + expect(heightResults.length).toEqual(1); + const heightResult = heightResults[0]; + expect(heightResult.transactionId).toEqual(transactionId); + assert(isConfirmedTransaction(heightResult), "Expected transaction to succeed"); + assert(heightResult.log, "Log must be available"); + const [firstHeightLog] = JSON.parse(heightResult.log); + expect(firstHeightLog.events.length).toEqual(2); + const heightTransaction = heightResult.transaction; + assert(isSendTransaction(heightTransaction), "Expected send transaction"); + expect(heightTransaction).toEqual(unsigned); + + connection.disconnect(); + }); + it("can search by minHeight and maxHeight", async () => { pendingWithoutWasmd(); const connection = await CosmWasmConnection.establish(httpUrl, defaultAddressPrefix, defaultConfig); @@ -980,47 +1098,4 @@ describe("CosmWasmConnection", () => { })().catch(done.fail); }); }); - - describe("integration tests", () => { - it("can send ERC20 tokens", async () => { - pendingWithoutWasmd(); - const connection = await CosmWasmConnection.establish(httpUrl, defaultAddressPrefix, defaultConfig); - const profile = new UserProfile(); - 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: senderAddress, - recipient: recipient, - memo: "My first payment", - amount: { - quantity: "75", - fractionalDigits: 0, - tokenTicker: "ISA" as TokenTicker, - }, - }); - 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)); - expect(blockInfo.state).toEqual(TransactionState.Succeeded); - - const recipientAccount = await connection.getAccount({ address: recipient }); - assert(recipientAccount, "Recipient account must have ISA tokens"); - expect(recipientAccount.balance).toEqual([ - { - tokenTicker: "ISA" as TokenTicker, - quantity: "75", - fractionalDigits: 0, - }, - ]); - - connection.disconnect(); - }); - }); }); diff --git a/packages/bcp/src/cosmwasmconnection.ts b/packages/bcp/src/cosmwasmconnection.ts index f26fa724..a7054e82 100644 --- a/packages/bcp/src/cosmwasmconnection.ts +++ b/packages/bcp/src/cosmwasmconnection.ts @@ -60,6 +60,16 @@ function isDefined(value: X | undefined): value is X { return value !== undefined; } +function deduplicate(input: ReadonlyArray, comparator: (a: T, b: T) => number): ReadonlyArray { + const out = new Array(); + for (const element of input) { + if (!out.find(o => comparator(o, element) === 0)) { + out.push(element); + } + } + return out; +} + /** Account and undefined are valid events. The third option means no event fired yet */ type LastWatchAccountEvent = Account | undefined | "no_event_fired_yet"; @@ -324,7 +334,35 @@ export class CosmWasmConnection implements BlockchainConnection { } else if (height) { txs = await this.cosmWasmClient.searchTx({ height: height }, filter); } else if (sentFromOrTo) { - txs = await this.cosmWasmClient.searchTx({ sentFromOrTo: sentFromOrTo }, filter); + const pendingRequests = new Array>(); + pendingRequests.push(this.cosmWasmClient.searchTx({ sentFromOrTo: sentFromOrTo }, filter)); + for (const contract of this.erc20Tokens.map(token => token.contractAddress)) { + const searchBySender = [ + { + key: "wasm.contract_address", + value: contract, + }, + { + key: "wasm.sender", + value: sentFromOrTo, + }, + ]; + const searchByRecipient = [ + { + key: "wasm.contract_address", + value: contract, + }, + { + key: "wasm.recipient", + value: sentFromOrTo, + }, + ]; + pendingRequests.push(this.cosmWasmClient.searchTx({ tags: searchBySender }, filter)); + pendingRequests.push(this.cosmWasmClient.searchTx({ tags: searchByRecipient }, filter)); + } + const responses = await Promise.all(pendingRequests); + const allResults = responses.reduce((accumulator, results) => accumulator.concat(results), []); + txs = deduplicate(allResults, (a, b) => a.txhash.localeCompare(b.txhash)); } else { throw new Error("Unsupported query"); }