diff --git a/.changeset/khaki-countries-act.md b/.changeset/khaki-countries-act.md new file mode 100644 index 0000000..99eee5b --- /dev/null +++ b/.changeset/khaki-countries-act.md @@ -0,0 +1,6 @@ +--- +'@rosen-chains/evm-rpc': major +'@rosen-chains/evm': major +--- + +consider transaction failure diff --git a/.changeset/quick-parrots-prove.md b/.changeset/quick-parrots-prove.md new file mode 100644 index 0000000..3da4578 --- /dev/null +++ b/.changeset/quick-parrots-prove.md @@ -0,0 +1,9 @@ +--- +'@rosen-chains/abstract-chain': major +'@rosen-chains/bitcoin': major +'@rosen-chains/cardano': major +'@rosen-chains/ergo': major +'@rosen-chains/evm': major +--- + +change `verifyLockTransactionExtraConditions` to async diff --git a/package-lock.json b/package-lock.json index f83784f..e33d234 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2071,11 +2071,11 @@ "license": "MIT" }, "node_modules/@cardano-ogmios/client": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/@cardano-ogmios/client/-/client-6.3.0.tgz", - "integrity": "sha512-nWaZ76n/R+p8nxBfRCetOuoDH8o5QToL5zWhRUu9EwHDJqM/0rzvYEk9JYCikAcVlC1qt6+3CcG4nCpG0dsptw==", + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/@cardano-ogmios/client/-/client-6.5.0.tgz", + "integrity": "sha512-hdianZyQ7r/mr0bfOQ5cZkeClF4hs1rz8R0JBwn+S/Iv0ElfcvhuOamy8whxc2n+rFnfVIyHoSoBHRMibyWKxw==", "dependencies": { - "@cardano-ogmios/schema": "6.3.0", + "@cardano-ogmios/schema": "6.5.0", "@cardanosolutions/json-bigint": "^1.0.1", "@types/json-bigint": "^1.0.1", "bech32": "^2.0.0", @@ -2084,7 +2084,7 @@ "isomorphic-ws": "^4.0.1", "nanoid": "^3.1.31", "ts-custom-error": "^3.2.0", - "ws": "^7.4.6" + "ws": "^7.5.10" }, "engines": { "node": ">=14" @@ -2099,9 +2099,9 @@ } }, "node_modules/@cardano-ogmios/client/node_modules/ws": { - "version": "7.5.9", - "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.9.tgz", - "integrity": "sha512-F+P9Jil7UiSKSkppIiD94dN07AwvFixvLIj1Og1Rl9GGMuNipJnV9JzjD6XuqmAeiswGvUmNLjr5cFuXwNS77Q==", + "version": "7.5.10", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.10.tgz", + "integrity": "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==", "engines": { "node": ">=8.3.0" }, @@ -2119,9 +2119,9 @@ } }, "node_modules/@cardano-ogmios/schema": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/@cardano-ogmios/schema/-/schema-6.3.0.tgz", - "integrity": "sha512-reM7NDYV4cgMAdFCzypoIuCVgSUfR9ztRMlk6p7k0cTeqUkbMfA83ps1FVkTDxzXxFjgM4EkhqoJyRjKIKRPQA==", + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/@cardano-ogmios/schema/-/schema-6.5.0.tgz", + "integrity": "sha512-tE2LvqfZ1SNBm4C6H/VITtFQ/zOJ8diGuHFPe30Pvg4hQzmD/1ekGCb73MJkcKy2WGpIRjnk/uwAzczc7Y320A==", "engines": { "node": ">=14" } @@ -5363,9 +5363,9 @@ ] }, "node_modules/@rosen-bridge/abstract-extractor": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/@rosen-bridge/abstract-extractor/-/abstract-extractor-0.1.0.tgz", - "integrity": "sha512-GYyPR5oAhGjrTHhS8gvNkdGCzaOYkp2FuPa8mCfl/BBMCrifmKx6n/yvDLrbhgurbOKx3MAnMJK5sLf63wAusg==", + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/@rosen-bridge/abstract-extractor/-/abstract-extractor-0.1.5.tgz", + "integrity": "sha512-+TIL0H88voDrZZg0ZdgdHPnIlrUq+m1n8IbMpCEGCs0EdTAcNXKhdK43vuuaMF7rDrunv8SdGEmzTB4GQnkloQ==", "dependencies": { "@rosen-bridge/abstract-logger": "^1.0.0", "@rosen-clients/ergo-explorer": "^1.1.1", @@ -5411,13 +5411,13 @@ } }, "node_modules/@rosen-bridge/evm-address-tx-extractor": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/@rosen-bridge/evm-address-tx-extractor/-/evm-address-tx-extractor-0.1.3.tgz", - "integrity": "sha512-BDrgA3o0n/kJbd0xwzIi+6Ou+mX9fzKJoecZkTAJVf6GFlWPKBfxAgMAHN2YqqK2oCxpOQU/AfW0qwqG68WXzw==", + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@rosen-bridge/evm-address-tx-extractor/-/evm-address-tx-extractor-1.0.0.tgz", + "integrity": "sha512-N2AC2Ei0Ip0amyj+cdnsE88hKvEmN7lpiV41HKTNCjJbiR8erdCqL61c/uId2Lso36E5ifYLZF+E9eKMDpX86g==", "dependencies": { - "@rosen-bridge/abstract-extractor": "^0.1.0", + "@rosen-bridge/abstract-extractor": "^0.1.5", "@rosen-bridge/abstract-logger": "^1.0.0", - "@rosen-bridge/scanner": "^4.0.0", + "@rosen-bridge/scanner": "^4.0.5", "ethers": "^6.11.0", "typeorm": "^0.3.20" }, @@ -5451,9 +5451,9 @@ } }, "node_modules/@rosen-bridge/rosen-extractor": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/@rosen-bridge/rosen-extractor/-/rosen-extractor-6.0.1.tgz", - "integrity": "sha512-ssWf+N/mpTXWX+rhe2OJNqivpsfRq07rqOpB5zK0LPo2RhM2s3U/SGd6Tk8YDumLxP4MSN7b2ZYYnIwvFRmDng==", + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/@rosen-bridge/rosen-extractor/-/rosen-extractor-6.1.0.tgz", + "integrity": "sha512-2wz4kJRLwqfN/8s/teVKNMfrNKc7/E22vJYQGBvicnqwHBnuL6GDDy41x/D/ck3+0qiG1NOTouCWGE382dRP7Q==", "dependencies": { "@blockfrost/blockfrost-js": "^5.4.0", "@cardano-ogmios/schema": "^6.0.3", @@ -5469,15 +5469,15 @@ } }, "node_modules/@rosen-bridge/scanner": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@rosen-bridge/scanner/-/scanner-4.0.0.tgz", - "integrity": "sha512-ZHFRfsv3YRyFEZY4VzEEeHRJm0bsAiocaq0HNuWOubSHydiaLra62NUfI1mdNDU+ql5c+JyE+cmv/1zm7tdPaw==", + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/@rosen-bridge/scanner/-/scanner-4.0.5.tgz", + "integrity": "sha512-TPX12mwpCz9elIkwzZgHoXa4M8Ho41rFomJ8hMvB9KgBCVJfq0UVUBUI6WWeAiFaKub66M8vzWBMoKl8kS6acA==", "dependencies": { "@apollo/client": "^3.8.7", "@blockfrost/blockfrost-js": "^5.4.0", "@cardano-ogmios/client": "^6.3.0", "@cardano-ogmios/schema": "^6.3.0", - "@rosen-bridge/abstract-extractor": "^0.1.0", + "@rosen-bridge/abstract-extractor": "^0.1.5", "@rosen-bridge/abstract-logger": "^1.0.0", "@rosen-bridge/json-bigint": "^0.1.0", "@rosen-clients/ergo-explorer": "^1.1.1", @@ -17492,7 +17492,7 @@ "@rosen-bridge/abstract-logger": "^1.0.0", "@rosen-bridge/json-bigint": "^0.1.0", "@rosen-bridge/minimum-fee": "^2.1.0", - "@rosen-bridge/rosen-extractor": "^6.0.1", + "@rosen-bridge/rosen-extractor": "^6.1.0", "@rosen-bridge/tokens": "^1.2.1", "blakejs": "^1.2.1" }, @@ -17520,7 +17520,7 @@ "@rosen-bridge/abstract-logger": "^1.0.0", "@rosen-bridge/bitcoin-utxo-selection": "^0.2.0", "@rosen-bridge/json-bigint": "^0.1.0", - "@rosen-bridge/rosen-extractor": "^6.0.1", + "@rosen-bridge/rosen-extractor": "^6.1.0", "@rosen-bridge/tokens": "^1.2.1", "bitcoinjs-lib": "^6.1.5" }, @@ -18293,7 +18293,7 @@ "@emurgo/cardano-serialization-lib-nodejs": "^11.3.1", "@rosen-bridge/abstract-logger": "^1.0.0", "@rosen-bridge/json-bigint": "^0.1.0", - "@rosen-bridge/rosen-extractor": "^6.0.1", + "@rosen-bridge/rosen-extractor": "^6.1.0", "@rosen-bridge/tokens": "^1.2.1", "bech32": "^2.0.0" }, @@ -18324,7 +18324,7 @@ "dependencies": { "@rosen-bridge/abstract-logger": "^1.0.0", "@rosen-bridge/json-bigint": "^0.1.0", - "@rosen-bridge/rosen-extractor": "^6.0.1", + "@rosen-bridge/rosen-extractor": "^6.1.0", "@rosen-bridge/tokens": "^1.2.1", "ergo-lib-wasm-nodejs": "^0.24.1" }, @@ -18354,7 +18354,7 @@ "dependencies": { "@rosen-bridge/abstract-logger": "^1.0.0", "@rosen-bridge/json-bigint": "^0.1.0", - "@rosen-bridge/rosen-extractor": "^6.0.1", + "@rosen-bridge/rosen-extractor": "^6.1.0", "@rosen-bridge/tokens": "^1.2.1", "ethers": "^6.11.1" }, @@ -20995,7 +20995,7 @@ "license": "GPL-3.0", "dependencies": { "@rosen-bridge/abstract-logger": "^1.0.0", - "@rosen-bridge/evm-address-tx-extractor": "^0.1.3", + "@rosen-bridge/evm-address-tx-extractor": "^1.0.0", "typeorm": "^0.3.20" }, "devDependencies": { diff --git a/packages/abstract-chain/lib/AbstractChain.ts b/packages/abstract-chain/lib/AbstractChain.ts index 5656052..a2da414 100644 --- a/packages/abstract-chain/lib/AbstractChain.ts +++ b/packages/abstract-chain/lib/AbstractChain.ts @@ -228,7 +228,7 @@ abstract class AbstractChain { `Failed in comparing event amount to fees: ${e}` ); } - if (this.verifyLockTransactionExtraConditions(tx, blockInfo)) { + if (await this.verifyLockTransactionExtraConditions(tx, blockInfo)) { this.logger.info( `Event [${eventId}] has been successfully validated` ); @@ -270,10 +270,10 @@ abstract class AbstractChain { * @param blockInfo * @returns true if the transaction is verified */ - verifyLockTransactionExtraConditions = ( + verifyLockTransactionExtraConditions = async ( transaction: TxType, blockInfo: BlockInfo - ): boolean => { + ): Promise => { throw Error( `You must implement 'verifyLockTransactionExtraConditions' or override 'verifyEvent' implementation` ); diff --git a/packages/abstract-chain/package.json b/packages/abstract-chain/package.json index e142827..a0bec70 100644 --- a/packages/abstract-chain/package.json +++ b/packages/abstract-chain/package.json @@ -23,7 +23,7 @@ "@rosen-bridge/abstract-logger": "^1.0.0", "@rosen-bridge/json-bigint": "^0.1.0", "@rosen-bridge/minimum-fee": "^2.1.0", - "@rosen-bridge/rosen-extractor": "^6.0.1", + "@rosen-bridge/rosen-extractor": "^6.1.0", "@rosen-bridge/tokens": "^1.2.1", "blakejs": "^1.2.1" }, diff --git a/packages/abstract-chain/tests/AbstractChain.spec.ts b/packages/abstract-chain/tests/AbstractChain.spec.ts index 0ab51af..9f956a3 100644 --- a/packages/abstract-chain/tests/AbstractChain.spec.ts +++ b/packages/abstract-chain/tests/AbstractChain.spec.ts @@ -273,7 +273,7 @@ describe('AbstractChain', () => { chain, 'verifyLockTransactionExtraConditions' ); - verifyLockTxSpy.mockReturnValueOnce(true); + verifyLockTxSpy.mockResolvedValueOnce(true); // run test const result = await chain.verifyEvent(event, feeConfig); @@ -517,7 +517,7 @@ describe('AbstractChain', () => { chain, 'verifyLockTransactionExtraConditions' ); - verifyLockTxSpy.mockReturnValueOnce(true); + verifyLockTxSpy.mockResolvedValueOnce(true); // run test const result = await chain.verifyEvent(event, feeConfig); @@ -597,7 +597,7 @@ describe('AbstractChain', () => { chain, 'verifyLockTransactionExtraConditions' ); - verifyLockTxSpy.mockReturnValueOnce(true); + verifyLockTxSpy.mockResolvedValueOnce(true); // run test const result = await chain.verifyEvent(event, fee); @@ -677,7 +677,7 @@ describe('AbstractChain', () => { chain, 'verifyLockTransactionExtraConditions' ); - verifyLockTxSpy.mockReturnValueOnce(true); + verifyLockTxSpy.mockResolvedValueOnce(true); // run test const result = await chain.verifyEvent(event, fee); @@ -746,7 +746,7 @@ describe('AbstractChain', () => { chain, 'verifyLockTransactionExtraConditions' ); - verifyLockTxSpy.mockReturnValueOnce(false); + verifyLockTxSpy.mockResolvedValueOnce(false); // run test const result = await chain.verifyEvent(event, feeConfig); diff --git a/packages/chains/bitcoin/lib/BitcoinChain.ts b/packages/chains/bitcoin/lib/BitcoinChain.ts index 5efbf17..56974b4 100644 --- a/packages/chains/bitcoin/lib/BitcoinChain.ts +++ b/packages/chains/bitcoin/lib/BitcoinChain.ts @@ -361,10 +361,10 @@ class BitcoinChain extends AbstractUtxoChain { * @param blockInfo * @returns true if the transaction is verified */ - verifyLockTransactionExtraConditions = ( + verifyLockTransactionExtraConditions = async ( transaction: BitcoinTx, blockInfo: BlockInfo - ): boolean => { + ): Promise => { return true; }; diff --git a/packages/chains/bitcoin/package.json b/packages/chains/bitcoin/package.json index 9048e12..cf08410 100644 --- a/packages/chains/bitcoin/package.json +++ b/packages/chains/bitcoin/package.json @@ -36,7 +36,7 @@ "@rosen-bridge/abstract-logger": "^1.0.0", "@rosen-bridge/bitcoin-utxo-selection": "^0.2.0", "@rosen-bridge/json-bigint": "^0.1.0", - "@rosen-bridge/rosen-extractor": "^6.0.1", + "@rosen-bridge/rosen-extractor": "^6.1.0", "@rosen-bridge/tokens": "^1.2.1", "bitcoinjs-lib": "^6.1.5" }, diff --git a/packages/chains/cardano/lib/CardanoChain.ts b/packages/chains/cardano/lib/CardanoChain.ts index 95e541c..d0c662f 100644 --- a/packages/chains/cardano/lib/CardanoChain.ts +++ b/packages/chains/cardano/lib/CardanoChain.ts @@ -544,10 +544,10 @@ class CardanoChain extends AbstractUtxoChain { * @param blockInfo * @returns true if the transaction is verified */ - verifyLockTransactionExtraConditions = ( + verifyLockTransactionExtraConditions = async ( transaction: CardanoTx, blockInfo: BlockInfo - ): boolean => { + ): Promise => { return true; }; diff --git a/packages/chains/cardano/package.json b/packages/chains/cardano/package.json index a1be78f..a87c141 100644 --- a/packages/chains/cardano/package.json +++ b/packages/chains/cardano/package.json @@ -22,7 +22,7 @@ "dependencies": { "@emurgo/cardano-serialization-lib-nodejs": "^11.3.1", "@rosen-bridge/abstract-logger": "^1.0.0", - "@rosen-bridge/rosen-extractor": "^6.0.1", + "@rosen-bridge/rosen-extractor": "^6.1.0", "@rosen-bridge/json-bigint": "^0.1.0", "@rosen-bridge/tokens": "^1.2.1", "bech32": "^2.0.0" diff --git a/packages/chains/ergo/lib/ErgoChain.ts b/packages/chains/ergo/lib/ErgoChain.ts index 349adc7..5341ecd 100644 --- a/packages/chains/ergo/lib/ErgoChain.ts +++ b/packages/chains/ergo/lib/ErgoChain.ts @@ -534,10 +534,10 @@ class ErgoChain extends AbstractUtxoChain { * @param blockInfo * @returns true if the transaction is verified */ - verifyLockTransactionExtraConditions = ( + verifyLockTransactionExtraConditions = async ( transaction: wasm.Transaction, blockInfo: BlockInfo - ): boolean => { + ): Promise => { const outputs = transaction.outputs(); for (let i = 0; i < outputs.len(); i++) { const box = outputs.get(i); diff --git a/packages/chains/ergo/package.json b/packages/chains/ergo/package.json index 4bab2ea..2bc5acc 100644 --- a/packages/chains/ergo/package.json +++ b/packages/chains/ergo/package.json @@ -22,7 +22,7 @@ "dependencies": { "@rosen-bridge/abstract-logger": "^1.0.0", "@rosen-bridge/json-bigint": "^0.1.0", - "@rosen-bridge/rosen-extractor": "^6.0.1", + "@rosen-bridge/rosen-extractor": "^6.1.0", "@rosen-bridge/tokens": "^1.2.1", "ergo-lib-wasm-nodejs": "^0.24.1" }, diff --git a/packages/chains/ergo/tests/ErgoChain.spec.ts b/packages/chains/ergo/tests/ErgoChain.spec.ts index ea6027f..d34aa96 100644 --- a/packages/chains/ergo/tests/ErgoChain.spec.ts +++ b/packages/chains/ergo/tests/ErgoChain.spec.ts @@ -1097,7 +1097,7 @@ describe('ErgoChain', () => { * @expected * - it should return false */ - it('should return false when output box creation height is more than a year ago', () => { + it('should return false when output box creation height is more than a year ago', async () => { const blockInfo: BlockInfo = { hash: ergoTestUtils.generateRandomId(), parentHash: ergoTestUtils.generateRandomId(), @@ -1108,7 +1108,7 @@ describe('ErgoChain', () => { ); const ergoChain = ergoTestUtils.generateChainObject(network); - const result = ergoChain.verifyLockTransactionExtraConditions( + const result = await ergoChain.verifyLockTransactionExtraConditions( mockedTx, blockInfo ); @@ -1127,7 +1127,7 @@ describe('ErgoChain', () => { * @expected * - it should return true */ - it('should return true when all output boxes creation heights are fresh', () => { + it('should return true when all output boxes creation heights are fresh', async () => { const blockInfo: BlockInfo = { hash: ergoTestUtils.generateRandomId(), parentHash: ergoTestUtils.generateRandomId(), @@ -1138,7 +1138,7 @@ describe('ErgoChain', () => { ); const ergoChain = ergoTestUtils.generateChainObject(network); - const result = ergoChain.verifyLockTransactionExtraConditions( + const result = await ergoChain.verifyLockTransactionExtraConditions( mockedTx, blockInfo ); diff --git a/packages/chains/evm/lib/EvmChain.ts b/packages/chains/evm/lib/EvmChain.ts index a54dd3b..e74346f 100644 --- a/packages/chains/evm/lib/EvmChain.ts +++ b/packages/chains/evm/lib/EvmChain.ts @@ -22,7 +22,7 @@ import { } from '@rosen-chains/abstract-chain'; import { EvmRosenExtractor } from '@rosen-bridge/rosen-extractor'; import AbstractEvmNetwork from './network/AbstractEvmNetwork'; -import { EvmConfigs, TssSignFunction } from './types'; +import { EvmConfigs, EvmTxStatus, TssSignFunction } from './types'; import { Signature, Transaction } from 'ethers'; import Serializer from './Serializer'; import * as EvmUtils from './EvmUtils'; @@ -434,6 +434,7 @@ abstract class EvmChain extends AbstractChain { /** * checks if a transaction is still valid and can be sent to the network + * - transaction should not be failed in blockchain * - transaction's nonce should be still available * @param transaction the transaction * @param signingStatus @@ -454,12 +455,27 @@ abstract class EvmChain extends AbstractChain { return { isValid: false, details: { - reason: `tx is failed in deserialization`, + reason: `failed to deserialize tx`, unexpected: false, }, }; } + // check if tx is failed + const txStatus = await this.network.getTransactionStatus(transaction.txId); + if (txStatus === EvmTxStatus.failed) { + this.logger.debug( + `Tx [${transaction.txId}] invalid: tx is failed in blockchain` + ); + return { + isValid: false, + details: { + reason: `tx is failed in blockchain`, + unexpected: true, + }, + }; + } + // check the nonce wasn't increased const nextNonce = await this.network.getAddressNextAvailableNonce( this.configs.addresses.lock @@ -728,14 +744,27 @@ abstract class EvmChain extends AbstractChain { /** * verifies additional conditions for a event lock transaction + * - the lock transaction should not be failed in the blockchain * @param transaction the lock transaction * @param blockInfo * @returns true if the transaction is verified */ - verifyLockTransactionExtraConditions = ( + verifyLockTransactionExtraConditions = async ( transaction: Transaction, blockInfo: BlockInfo - ): boolean => { + ): Promise => { + // check if tx is failed + if (!transaction.hash) + throw new ImpossibleBehavior( + `failed to get txId of the lock transaction while verifying the event` + ); + const txStatus = await this.network.getTransactionStatus(transaction.hash); + if (txStatus !== EvmTxStatus.succeed) { + this.logger.debug( + `Lock tx [${transaction.hash}] is not succeed (failed or unexpected status)` + ); + return false; + } return true; }; diff --git a/packages/chains/evm/lib/network/AbstractEvmNetwork.ts b/packages/chains/evm/lib/network/AbstractEvmNetwork.ts index 1de9b74..6a48016 100644 --- a/packages/chains/evm/lib/network/AbstractEvmNetwork.ts +++ b/packages/chains/evm/lib/network/AbstractEvmNetwork.ts @@ -1,5 +1,6 @@ import { AbstractChainNetwork } from '@rosen-chains/abstract-chain'; -import { Transaction } from 'ethers'; +import { Transaction, TransactionResponse } from 'ethers'; +import { EvmTxStatus } from '../types'; abstract class AbstractEvmNetwork extends AbstractChainNetwork { /** @@ -57,6 +58,13 @@ abstract class AbstractEvmNetwork extends AbstractChainNetwork { getMempoolTransactions = async (): Promise> => { return []; }; + + /** + * gets the transaction status (mempool, succeed, failed) + * @param hash the unsigned hash or ID of the transaction + * @returns the transaction status + */ + abstract getTransactionStatus: (hash: string) => Promise; } export default AbstractEvmNetwork; diff --git a/packages/chains/evm/lib/types.ts b/packages/chains/evm/lib/types.ts index 7b06e74..5bc634c 100644 --- a/packages/chains/evm/lib/types.ts +++ b/packages/chains/evm/lib/types.ts @@ -11,3 +11,10 @@ export type TssSignFunction = (txHash: Uint8Array) => Promise<{ signature: string; signatureRecovery: string; }>; + +export enum EvmTxStatus { + failed = 'failed', + succeed = 'succeed', + mempool = 'mempool', + notFound = 'not-found', +} diff --git a/packages/chains/evm/package.json b/packages/chains/evm/package.json index 82f3533..d7ab8d2 100644 --- a/packages/chains/evm/package.json +++ b/packages/chains/evm/package.json @@ -35,7 +35,7 @@ "dependencies": { "@rosen-bridge/abstract-logger": "^1.0.0", "@rosen-bridge/json-bigint": "^0.1.0", - "@rosen-bridge/rosen-extractor": "^6.0.1", + "@rosen-bridge/rosen-extractor": "^6.1.0", "@rosen-bridge/tokens": "^1.2.1", "ethers": "^6.11.1" }, diff --git a/packages/chains/evm/tests/EvmChain.spec.ts b/packages/chains/evm/tests/EvmChain.spec.ts index c083fda..0ca554b 100644 --- a/packages/chains/evm/tests/EvmChain.spec.ts +++ b/packages/chains/evm/tests/EvmChain.spec.ts @@ -14,6 +14,7 @@ import * as testUtils from './TestUtils'; import Serializer from '../lib/Serializer'; import { Transaction, TransactionLike } from 'ethers'; import { mockGetAddressBalanceForNativeToken } from './TestUtils'; +import { EvmTxStatus } from '../lib'; describe('EvmChain', () => { const network = new TestEvmNetwork(); @@ -1825,7 +1826,7 @@ describe('EvmChain', () => { describe('isTxValid', () => { /** * @target EvmChain.isTxValid should return true when - * nonce is not used + * tx is not found and nonce is not used * @dependencies * @scenario * - mock PaymentTransaction @@ -1835,7 +1836,7 @@ describe('EvmChain', () => { * @expected * - it should return true with no details */ - it('should return true when nonce is not used', async () => { + it('should return true when tx is not found and nonce is not used', async () => { // mock PaymentTransaction const eventId = 'bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb'; const txType = TransactionType.payment; @@ -1849,6 +1850,9 @@ describe('EvmChain', () => { txType ); + // mock getTransactionStatus + testUtils.mockGetTransactionStatus(network, EvmTxStatus.notFound); + // mock getAddressNextAvailableNonce testUtils.mockGetAddressNextAvailableNonce(network, tx.nonce); @@ -1867,6 +1871,7 @@ describe('EvmChain', () => { * @dependencies * @scenario * - mock PaymentTransaction + * - mock getTransactionStatus * - mock getAddressNextAvailableNonce * - call the function * - check returned value @@ -1887,6 +1892,9 @@ describe('EvmChain', () => { txType ); + // mock getTransactionStatus + testUtils.mockGetTransactionStatus(network, EvmTxStatus.succeed); + // mock getAddressNextAvailableNonce testUtils.mockGetAddressNextAvailableNonce(network, tx.nonce + 1); @@ -1902,6 +1910,52 @@ describe('EvmChain', () => { }, }); }); + + /** + * @target EvmChain.isTxValid should return false when + * tx is failed + * @dependencies + * @scenario + * - mock PaymentTransaction + * - mock getTransactionStatus + * - mock getAddressNextAvailableNonce + * - call the function + * - check returned value + * @expected + * - it should return false and as unexpected invalidation + */ + it('should return false when tx is failed', async () => { + // mock PaymentTransaction + const eventId = 'bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb'; + const txType = TransactionType.payment; + const tx = Transaction.from(TestData.erc20transaction as TransactionLike); + + const paymentTx = new PaymentTransaction( + evmChain.CHAIN, + tx.unsignedHash, + eventId, + Serializer.serialize(tx), + txType + ); + + // mock getTransactionStatus + testUtils.mockGetTransactionStatus(network, EvmTxStatus.failed); + + // mock getAddressNextAvailableNonce + testUtils.mockGetAddressNextAvailableNonce(network, tx.nonce); + + // run test + const result = await evmChain.isTxValid(paymentTx, SigningStatus.Signed); + + // check returned value + expect(result).toEqual({ + isValid: false, + details: { + reason: expect.any(String), + unexpected: true, + }, + }); + }); }); describe('signTransaction', () => { @@ -2097,4 +2151,64 @@ describe('EvmChain', () => { }); }); }); + + describe('verifyLockTransactionExtraConditions', () => { + /** + * @target EvmChain.verifyLockTransactionExtraConditions should return true when + * tx is succeed + * @dependencies + * @scenario + * - mock transaction + * - mock getTransactionStatus + * - call the function + * - check returned value + * @expected + * - it should return true + */ + it('should return true when tx is succeed', async () => { + // mock transaction + const tx = Transaction.from(TestData.erc20transaction as TransactionLike); + + // mock getTransactionStatus + testUtils.mockGetTransactionStatus(network, EvmTxStatus.succeed); + + // run test + const result = await evmChain.verifyLockTransactionExtraConditions( + tx, + {} as any + ); + + // check returned value + expect(result).toEqual(true); + }); + + /** + * @target EvmChain.verifyLockTransactionExtraConditions should return false when + * tx is failed + * @dependencies + * @scenario + * - mock transaction + * - mock getTransactionStatus + * - call the function + * - check returned value + * @expected + * - it should return false + */ + it('should return false when tx is failed', async () => { + // mock transaction + const tx = Transaction.from(TestData.erc20transaction as TransactionLike); + + // mock getTransactionStatus + testUtils.mockGetTransactionStatus(network, EvmTxStatus.failed); + + // run test + const result = await evmChain.verifyLockTransactionExtraConditions( + tx, + {} as any + ); + + // check returned value + expect(result).toEqual(false); + }); + }); }); diff --git a/packages/chains/evm/tests/TestUtils.ts b/packages/chains/evm/tests/TestUtils.ts index ffda8f1..594689b 100644 --- a/packages/chains/evm/tests/TestUtils.ts +++ b/packages/chains/evm/tests/TestUtils.ts @@ -1,5 +1,5 @@ import * as testData from './testData'; -import { EvmConfigs, TssSignFunction } from '../lib/types'; +import { EvmConfigs, EvmTxStatus, TssSignFunction } from '../lib/types'; import EvmChain from '../lib/EvmChain'; import { vi } from 'vitest'; import { AbstractEvmNetwork } from '../lib'; @@ -107,3 +107,10 @@ export const generateChainObjectWithMultiDecimalTokenMap = ( signFn ); }; + +export const mockGetTransactionStatus = ( + network: AbstractEvmNetwork, + result: EvmTxStatus +) => { + spyOn(network, 'getTransactionStatus').mockResolvedValue(result); +}; diff --git a/packages/chains/evm/tests/network/TestEvmNetwork.ts b/packages/chains/evm/tests/network/TestEvmNetwork.ts index 55ffdc8..f1e586c 100644 --- a/packages/chains/evm/tests/network/TestEvmNetwork.ts +++ b/packages/chains/evm/tests/network/TestEvmNetwork.ts @@ -1,5 +1,5 @@ import { Transaction } from 'ethers'; -import { AbstractEvmNetwork } from '../../lib'; +import { AbstractEvmNetwork, EvmTxStatus } from '../../lib'; import { BlockInfo, AssetBalance, @@ -73,6 +73,10 @@ class TestEvmNetwork extends AbstractEvmNetwork { getMaxFeePerGas = (): Promise => { throw Error('Not mocked'); }; + + getTransactionStatus = (hash: string): Promise => { + throw Error('Not mocked'); + }; } export default TestEvmNetwork; diff --git a/packages/networks/evm-rpc/lib/EvmRpcNetwork.ts b/packages/networks/evm-rpc/lib/EvmRpcNetwork.ts index bf43df1..5cb3156 100644 --- a/packages/networks/evm-rpc/lib/EvmRpcNetwork.ts +++ b/packages/networks/evm-rpc/lib/EvmRpcNetwork.ts @@ -7,7 +7,11 @@ import { UnexpectedApiError, } from '@rosen-chains/abstract-chain'; import JsonBigInt from '@rosen-bridge/json-bigint'; -import { AbstractEvmNetwork, PartialERC20ABI } from '@rosen-chains/evm'; +import { + AbstractEvmNetwork, + EvmTxStatus, + PartialERC20ABI, +} from '@rosen-chains/evm'; import { Block, JsonRpcProvider, @@ -15,6 +19,7 @@ import { TransactionResponse, ethers, FeeData, + isCallException, } from 'ethers'; import { DataSource } from 'typeorm'; import AddressTxAction from './AddressTxAction'; @@ -66,20 +71,27 @@ class EvmRpcNetwork extends AbstractEvmNetwork { const transactionId = txRecord === null ? hash : txRecord.signedHash; // get transaction confirmation + const baseError = `Failed to get transaction [${transactionId}] from ${this.chain} RPC: `; + let tx: TransactionResponse | null; try { - const tx = await this.provider.getTransaction(transactionId); + tx = await this.provider.getTransaction(transactionId); this.logger.debug( `requested 'getTransaction' of ${ this.chain } RPC with id [${transactionId}]. res: ${JsonBigInt.stringify(tx)}` ); - if (!tx) { - this.logger.debug(`Transaction [${transactionId}] is not found`); - return -1; - } - return await tx.confirmations(); } catch (e: unknown) { - const baseError = `Failed to get transaction [${transactionId}] from ${this.chain} RPC: `; + throw new UnexpectedApiError(baseError + `${e}`); + } + if (!tx) { + this.logger.debug(`Transaction [${transactionId}] is not found`); + return -1; + } + try { + const status = await this.getStatus(tx); + if (status === EvmTxStatus.succeed) return await tx.confirmations(); + else return -1; + } catch (e) { throw new UnexpectedApiError(baseError + `${e}`); } }; @@ -342,6 +354,55 @@ class EvmRpcNetwork extends AbstractEvmNetwork { throw new UnexpectedApiError(baseError + `maxFeePerGas is null`); return feeData.maxFeePerGas; }; + + /** + * gets the transaction status (mempool, succeed, failed) + * Note: this function considers the hash as unsigned hash + * if the tx was not found, considers it as TxId (signed hash) + * @param hash the unsigned hash or ID of the transaction + * @returns the transaction status + */ + getTransactionStatus = async (hash: string): Promise => { + // check if hash is representing signed or unsigned version of the tx + const txRecord = await this.dbAction.getTxByUnsignedHash(hash); + const transactionId = txRecord === null ? hash : txRecord.signedHash; + + const baseError = `Failed to get transaction [${transactionId}] from ${this.chain} RPC: `; + let tx: TransactionResponse | null; + try { + tx = await this.provider.getTransaction(transactionId); + this.logger.debug( + `requested 'getTransaction' of ${ + this.chain + } RPC with id [${transactionId}]. res: ${JsonBigInt.stringify(tx)}` + ); + } catch (e: unknown) { + throw new UnexpectedApiError(baseError + `${e}`); + } + if (!tx) return EvmTxStatus.notFound; + try { + return await this.getStatus(tx); + } catch (e) { + throw new UnexpectedApiError(baseError + `${e}`); + } + }; + + /** + * gets the transaction status (mempool, succeed, failed) from TransactionResponse object + * @param TransactionResponse + */ + protected getStatus = async ( + tx: TransactionResponse + ): Promise => { + try { + const result = await tx.wait(0); + if (result) return EvmTxStatus.succeed; + else return EvmTxStatus.mempool; + } catch (e) { + if (isCallException(e)) return EvmTxStatus.failed; + else throw e; + } + }; } export default EvmRpcNetwork; diff --git a/packages/networks/evm-rpc/package.json b/packages/networks/evm-rpc/package.json index 9ffe476..abc65aa 100644 --- a/packages/networks/evm-rpc/package.json +++ b/packages/networks/evm-rpc/package.json @@ -34,7 +34,7 @@ }, "dependencies": { "@rosen-bridge/abstract-logger": "^1.0.0", - "@rosen-bridge/evm-address-tx-extractor": "^0.1.3", + "@rosen-bridge/evm-address-tx-extractor": "^1.0.0", "typeorm": "^0.3.20" }, "peerDependencies": { diff --git a/packages/networks/evm-rpc/tests/EvmRpcNetwork.spec.ts b/packages/networks/evm-rpc/tests/EvmRpcNetwork.spec.ts index f859899..33bd246 100644 --- a/packages/networks/evm-rpc/tests/EvmRpcNetwork.spec.ts +++ b/packages/networks/evm-rpc/tests/EvmRpcNetwork.spec.ts @@ -8,6 +8,7 @@ import { mockDataSource } from './mocked/dataSource.mock'; import { TestEvmRpcNetwork } from './TestEvmRpcNetwork'; import * as testData from './testData'; import { ContractInstance } from './mocked/ethers.mock'; +import { EvmTxStatus } from '@rosen-chains/evm'; describe('EvmRpcNetwork', () => { let network: TestEvmRpcNetwork; @@ -51,9 +52,12 @@ describe('EvmRpcNetwork', () => { /** * @target `EvmRpcNetwork.getTxConfirmation` should fetch confirmation using unsigned hash successfully * @dependencies + * - database * @scenario * - insert transaction with expected unsigned hash into database - * - mock provider.`getTransaction`.`confirmations` to return confirmation + * - mock provider.`getTransaction` + * - `wait` to return the transaction + * - `confirmations` to return confirmation * - run test * - check returned value * - check function is called @@ -72,12 +76,15 @@ describe('EvmRpcNetwork', () => { address: testData.lockAddress, blockId: 'blockId', extractor: 'custom-extractor', + status: 'succeed', }); const mockedConfirmation = 60; const transactionInstance = { + wait: vi.fn(), confirmations: vi.fn(), }; + transactionInstance.wait.mockResolvedValue(testData.transaction0); transactionInstance.confirmations.mockResolvedValue(mockedConfirmation); const getTransactionSpy = vi.spyOn( network.getProvider(), @@ -94,7 +101,9 @@ describe('EvmRpcNetwork', () => { * @target `EvmRpcNetwork.getTxConfirmation` should fetch confirmation using txId successfully * @dependencies * @scenario - * - mock provider.`getTransaction`.`confirmations` to return confirmation + * - mock provider.`getTransaction` + * - `wait` to return the transaction + * - `confirmations` to return confirmation * - run test * - check returned value * - check function is called @@ -107,8 +116,10 @@ describe('EvmRpcNetwork', () => { const mockedConfirmation = 60; const transactionInstance = { + wait: vi.fn(), confirmations: vi.fn(), }; + transactionInstance.wait.mockResolvedValue(testData.transaction0); transactionInstance.confirmations.mockResolvedValue(mockedConfirmation); const getTransactionSpy = vi.spyOn( network.getProvider(), @@ -125,7 +136,8 @@ describe('EvmRpcNetwork', () => { * @target `EvmRpcNetwork.getTxConfirmation` should return -1 when transaction is not found * @dependencies * @scenario - * - mock provider.`getTransaction` to return null + * - mock provider.`getTransaction` + * - `confirmations` to return null * - run test * - check returned value * - check function is called @@ -146,6 +158,89 @@ describe('EvmRpcNetwork', () => { expect(result).toEqual(-1); expect(getTransactionSpy).toHaveBeenCalledWith(txId); }); + + /** + * @target `EvmRpcNetwork.getTxConfirmation` should return -1 for failed tx using unsigned hash + * @dependencies + * - database + * @scenario + * - insert transaction with expected unsigned hash into database + * - mock provider.`getTransaction` + * - `wait` to return null + * - `confirmations` to return confirmation + * - run test + * - check returned value + * - check function is called + * @expected + * - it should be mocked confirmation + * - provider.`getTransaction` should have been called with signedHash + */ + it('should return -1 for failed tx using unsigned hash', async () => { + const unsignedHash = generateRandomId(); + const signedHash = generateRandomId(); + + await addressTxRepository.insert({ + unsignedHash: unsignedHash, + signedHash: signedHash, + nonce: 0, + address: testData.lockAddress, + blockId: 'blockId', + extractor: 'custom-extractor', + status: 'failed', + }); + + const mockedConfirmation = 60; + const transactionInstance = { + wait: vi.fn(), + confirmations: vi.fn(), + }; + transactionInstance.wait.mockResolvedValue(null); + transactionInstance.confirmations.mockResolvedValue(mockedConfirmation); + const getTransactionSpy = vi.spyOn( + network.getProvider(), + 'getTransaction' + ); + getTransactionSpy.mockResolvedValue(transactionInstance as any); + + const result = await network.getTxConfirmation(unsignedHash); + expect(result).toEqual(-1); + expect(getTransactionSpy).toHaveBeenCalledWith(signedHash); + }); + + /** + * @target `EvmRpcNetwork.getTxConfirmation` should return -1 for failed tx using signed hash + * @dependencies + * @scenario + * - mock provider.`getTransaction` + * - `wait` to return null + * - `confirmations` to return confirmation + * - run test + * - check returned value + * - check function is called + * @expected + * - it should be mocked confirmation + * - provider.`getTransaction` should have been called with txId + */ + it('should return -1 for failed tx using signed hash', async () => { + const txId = generateRandomId(); + + const mockedConfirmation = 60; + const transactionInstance = { + wait: vi.fn(), + confirmations: vi.fn(), + }; + transactionInstance.wait.mockResolvedValue(null); + transactionInstance.confirmations.mockResolvedValue(mockedConfirmation); + const getTransactionSpy = vi.spyOn( + network.getProvider(), + 'getTransaction' + ); + getTransactionSpy.mockResolvedValue(transactionInstance as any); + + const result = await network.getTxConfirmation(txId); + expect(result).toEqual(-1); + expect(getTransactionSpy).toHaveBeenCalledWith(txId); + }); }); describe('getBlockTransactionIds', () => { @@ -405,4 +500,123 @@ describe('EvmRpcNetwork', () => { expect(result).toEqual(testData.maxFeePerGas); }); }); + + describe('getTransactionStatus', () => { + /** + * @target `EvmRpcNetwork.getTransactionStatus` should return not found successfully + * @dependencies + * @scenario + * - mock provider.`getTransaction` to return null + * - run test + * - check returned value + * - check function is called + * @expected + * - it should be mocked confirmation + * - provider.`getTransaction` should have been called with signedHash + */ + it('should return not found successfully', async () => { + const hash = generateRandomId(); + + const getTransactionSpy = vi.spyOn( + network.getProvider(), + 'getTransaction' + ); + getTransactionSpy.mockResolvedValue(null); + + const result = await network.getTransactionStatus(hash); + expect(result).toEqual(EvmTxStatus.notFound); + }); + + /** + * @target `EvmRpcNetwork.getTransactionStatus` should return succeed successfully + * @dependencies + * @scenario + * - mock provider.`getTransaction`.`wait` to return the transaction + * - run test + * - check returned value + * - check function is called + * @expected + * - it should be mocked confirmation + * - provider.`getTransaction` should have been called with signedHash + */ + it('should return succeed successfully', async () => { + const hash = generateRandomId(); + + const transactionInstance = { + wait: vi.fn(), + confirmations: vi.fn(), + }; + transactionInstance.wait.mockResolvedValue(testData.transaction0); + const getTransactionSpy = vi.spyOn( + network.getProvider(), + 'getTransaction' + ); + getTransactionSpy.mockResolvedValue(transactionInstance as any); + + const result = await network.getTransactionStatus(hash); + expect(result).toEqual(EvmTxStatus.succeed); + }); + + /** + * @target `EvmRpcNetwork.getTransactionStatus` should return mempool successfully + * @dependencies + * @scenario + * - mock provider.`getTransaction`.`wait` to return null + * - run test + * - check returned value + * - check function is called + * @expected + * - it should be mocked confirmation + * - provider.`getTransaction` should have been called with signedHash + */ + it('should return mempool successfully', async () => { + const hash = generateRandomId(); + + const transactionInstance = { + wait: vi.fn(), + confirmations: vi.fn(), + }; + transactionInstance.wait.mockResolvedValue(null); + const getTransactionSpy = vi.spyOn( + network.getProvider(), + 'getTransaction' + ); + getTransactionSpy.mockResolvedValue(transactionInstance as any); + + const result = await network.getTransactionStatus(hash); + expect(result).toEqual(EvmTxStatus.mempool); + }); + + /** + * @target `EvmRpcNetwork.getTransactionStatus` should return failed when it throws CallbackException + * @dependencies + * @scenario + * - mock provider.`getTransaction`.`wait` to return null + * - run test + * - check returned value + * - check function is called + * @expected + * - it should be mocked confirmation + * - provider.`getTransaction` should have been called with signedHash + */ + it('should return failed when it throws CallbackException', async () => { + const hash = generateRandomId(); + + const transactionInstance = { + wait: vi.fn(), + confirmations: vi.fn(), + }; + transactionInstance.wait.mockRejectedValue({ + code: 'CALL_EXCEPTION', + }); + const getTransactionSpy = vi.spyOn( + network.getProvider(), + 'getTransaction' + ); + getTransactionSpy.mockResolvedValue(transactionInstance as any); + + const result = await network.getTransactionStatus(hash); + expect(result).toEqual(EvmTxStatus.failed); + }); + }); });