From 3f5ca30c004ea0aa23aa23488b7fa556a2afca7e Mon Sep 17 00:00:00 2001 From: Gustavo Antunes Date: Thu, 9 Sep 2021 12:03:09 -0300 Subject: [PATCH] [FEATURE] Manage transaction state (#582) --- src/transaction/TransactionController.test.ts | 530 ++++------------- src/transaction/TransactionController.ts | 201 ++++++- src/transaction/mocks/txsMock.ts | 536 ++++++++++++++++++ src/util.test.ts | 20 +- src/util.ts | 59 +- 5 files changed, 877 insertions(+), 469 deletions(-) create mode 100644 src/transaction/mocks/txsMock.ts diff --git a/src/transaction/TransactionController.test.ts b/src/transaction/TransactionController.test.ts index f69e3488d0..a68429b765 100644 --- a/src/transaction/TransactionController.test.ts +++ b/src/transaction/TransactionController.test.ts @@ -10,6 +10,14 @@ import { TransactionStatus, TransactionMeta, } from './TransactionController'; +import { + ethTxsMock, + tokenTxsMock, + txsInStateMock, + txsInStateWithOutdatedStatusMock, + txsInStateWithOutdatedGasDataMock, + txsInStateWithOutdatedStatusAndGasDataMock, +} from './mocks/txsMock'; const globalAny: any = global; @@ -142,411 +150,29 @@ const TOKEN_TRANSACTION_HASH = const ETHER_TRANSACTION_HASH = '0xa9d17df83756011ea63e1f0ca50a6627df7cac9806809e36680fcf4e88cb9dae'; -const ETH_TRANSACTIONS = [ - { - blockNumber: '4535101', - confirmations: '10', - contractAddress: '', - cumulativeGasUsed: '120607', - from: '0xe46abaf75cfbff815c0b7ffed6f02b0760ea27f1', - gas: '335208', - gasPrice: '10000000000', - gasUsed: '21000', - hash: ETHER_TRANSACTION_HASH, - input: '0x', - isError: '0', - nonce: '9', - timeStamp: '1543596286', - to: '0x6bf137f335ea1b8f193b8f6ea92561a60d23a207', - transactionIndex: '2', - txreceipt_status: '1', - value: '100000000000000000', - }, - { - blockNumber: '4535108', - confirmations: '3', - contractAddress: '', - cumulativeGasUsed: '693910', - from: '0x6bf137f335ea1b8f193b8f6ea92561a60d23a207', - gas: '335208', - gasPrice: '20000000000', - gasUsed: '21000', - hash: '0x342e9d73e10004af41d04973339fc7219dbadcbb5629730cfe65e9f9cb15ff92', - input: '0x', - isError: '0', - nonce: '0', - timeStamp: '1543596378', - to: '0xb2d191b6fe03c5b8a1ab249cfe88c37553357a23', - transactionIndex: '12', - txreceipt_status: '1', - value: '50000000000000000', - }, - { - blockNumber: '4535105', - confirmations: '4', - contractAddress: '', - cumulativeGasUsed: '693910', - from: '0x6bf137f335ea1b8f193b8f6ea92561a60d23a207', - gas: '335208', - gasPrice: '20000000000', - gasUsed: '21000', - hash: '0x342e9d73e10004af41d04973339fc7219dbadcbb5629730cfe65e9f9cb15ff91', - input: '0x', - isError: '0', - nonce: '1', - timeStamp: '1543596356', - transactionIndex: '13', - txreceipt_status: '1', - value: '50000000000000000', - }, - { - blockNumber: '4535106', - confirmations: '4', - contractAddress: '', - cumulativeGasUsed: '693910', - from: '0x6bf137f335ea1b8f193b8f6ea92561a60d23a207', - gas: '335208', - gasPrice: '20000000000', - gasUsed: '21000', - hash: '0x342e9d73e10004af41d04973139fc7219dbadcbb5629730cfe65e9f9cb15ff91', - input: '0x11', - isError: '0', - nonce: '3', - timeStamp: '1543596356', - to: '0xb2d191b6fe03c5b8a1ab249cfe88c37553357a23', - transactionIndex: '13', - txreceipt_status: '1', - value: '50000000000000000', - }, -]; - -const TOKEN_TRANSACTIONS = [ - { - blockNumber: '8222239', - timeStamp: '1564091067', - hash: TOKEN_TRANSACTION_HASH, - nonce: '2329', - blockHash: - '0x3c30a9be9aea7be13caad419444140c11839d72e70479ec7e9c6d8bd08c533bc', - from: '0xdfa6edae2ec0cf1d4a60542422724a48195a5071', - contractAddress: '0x89d24a6b4ccb1b6faa2625fe562bdd9a23260359', - to: '0x6bf137f335ea1b8f193b8f6ea92561a60d23a207', - value: '0', - tokenName: 'Sai Stablecoin v1.0', - tokenSymbol: 'SAI', - tokenDecimal: '18', - transactionIndex: '69', - gas: '624874', - gasPrice: '20000000000', - gasUsed: '609874', - cumulativeGasUsed: '3203881', - input: 'deprecated', - confirmations: '3659676', - }, - { - blockNumber: '8222250', - timeStamp: '1564091247', - hash: '0xdcd1c8bee545d3f76d80b20a23ad44276ba2e376681228eb4570cf3518491279', - nonce: '2330', - blockHash: - '0x16986dd66bedb20a5b846ec2b6c0ecaa62f1c4b51fac58c1326101fd9126dd82', - from: '0xdfa6edae2ec0cf1d4a60542422724a48195a5071', - contractAddress: '0x89d24a6b4ccb1b6faa2625fe562bdd9a23260359', - to: '0x6bf137f335ea1b8f193b8f6ea92561a60d23a207', - value: '0', - tokenName: 'Sai Stablecoin v1.0', - tokenSymbol: 'SAI', - tokenDecimal: '18', - transactionIndex: '40', - gas: '594268', - gasPrice: '20000000000', - gasUsed: '579268', - cumulativeGasUsed: '2009011', - input: 'deprecated', - confirmations: '3659665', - }, - { - blockNumber: '8223771', - timeStamp: '1564111652', - hash: '0x070369e6f560b0deca52e050ff1a961fa7b688bbec5cea08435921c9d9b0f52e', - nonce: '2333', - blockHash: - '0x0aff8b36881be99df6d176d7c64c2171672c0483684a10c112d2c90fefe30a0a', - from: '0xdfa6edae2ec0cf1d4a60542422724a48195a5071', - contractAddress: '0x89d24a6b4ccb1b6faa2625fe562bdd9a23260359', - to: '0x6bf137f335ea1b8f193b8f6ea92561a60d23a207', - value: '0', - tokenName: 'Sai Stablecoin v1.0', - tokenSymbol: 'SAI', - tokenDecimal: '18', - transactionIndex: '132', - gas: '583810', - gasPrice: '6000000000', - gasUsed: '568810', - cumulativeGasUsed: '6956245', - input: 'deprecated', - confirmations: '3658144', - }, - { - blockNumber: '8224850', - timeStamp: '1564126442', - hash: '0x8ef20ec9597c8c2e945bcc76d2668e5d3bb088b081fe8c5b5af2e1cbd315a20f', - nonce: '31', - blockHash: - '0xb80d4d861ecb7a3cb14e591c0aaeb226842d0267772affa2acc1a590c7535647', - from: '0x6c70e3563cef0c6835703bb2664c9f59a92353e4', - contractAddress: '0x89d24a6b4ccb1b6faa2625fe562bdd9a23260359', - to: '0x6bf137f335ea1b8f193b8f6ea92561a60d23a207', - value: '10000000000000000000', - tokenName: 'Sai Stablecoin v1.0', - tokenSymbol: 'SAI', - tokenDecimal: '18', - transactionIndex: '169', - gas: '78447', - gasPrice: '2000000000', - gasUsed: '52298', - cumulativeGasUsed: '7047823', - input: 'deprecated', - confirmations: '3657065', - }, - { - blockNumber: '8228053', - timeStamp: '1564168901', - hash: '0xa0f2d7b558bb3cc28fa568f6feb8ed30eb28a01a674d7c0d4ae603fc691e6020', - nonce: '2368', - blockHash: - '0x62c515ea049842c968ca67499f47a32a11394364d319d9c9cc0a0211652a7294', - from: '0xdfa6edae2ec0cf1d4a60542422724a48195a5071', - contractAddress: '0x89d24a6b4ccb1b6faa2625fe562bdd9a23260359', - to: '0x6bf137f335ea1b8f193b8f6ea92561a60d23a207', - value: '0', - tokenName: 'Sai Stablecoin v1.0', - tokenSymbol: 'SAI', - tokenDecimal: '18', - transactionIndex: '43', - gas: '567156', - gasPrice: '3000000000', - gasUsed: '552156', - cumulativeGasUsed: '3048261', - input: 'deprecated', - confirmations: '3653862', - }, - { - blockNumber: '8315335', - timeStamp: '1565339223', - hash: '0x464df60fe00b6dd04c9e8ab341d02af9b10a619d2fcd60fd2971f10edf12118f', - nonce: '206760', - blockHash: - '0x98275388ef6708debe35ac7bf2e30143c9b1fd9e0e457ca03598fc1f4209e273', - from: '0x00cfbbaf7ddb3a1476767101c12a0162e241fbad', - contractAddress: '0x4dc3643dbc642b72c158e7f3d2ff232df61cb6ce', - to: '0x6bf137f335ea1b8f193b8f6ea92561a60d23a207', - value: '100000000000000000', - tokenName: 'Amber', - tokenSymbol: 'AMB', - tokenDecimal: '18', - transactionIndex: '186', - gas: '60000', - gasPrice: '2000000000', - gasUsed: '52108', - cumulativeGasUsed: '7490707', - input: 'deprecated', - confirmations: '3566580', - }, - { - blockNumber: '8350846', - timeStamp: '1565815049', - hash: '0xc0682327ad3efd56dfa33e8206b4e09efad4e419a6191076069d217e3ee2341f', - nonce: '2506', - blockHash: - '0xd0aa3c0e319fdfeb21b0192cf77b9760b8668060a5977a5f10f8413531083afa', - from: '0xdfa6edae2ec0cf1d4a60542422724a48195a5071', - contractAddress: '0x89d24a6b4ccb1b6faa2625fe562bdd9a23260359', - to: '0x6bf137f335ea1b8f193b8f6ea92561a60d23a207', - value: '4', - tokenName: 'Sai Stablecoin v1.0', - tokenSymbol: 'SAI', - tokenDecimal: '18', - transactionIndex: '48', - gas: '578737', - gasPrice: '3000000000', - gasUsed: '518737', - cumulativeGasUsed: '2848015', - input: 'deprecated', - confirmations: '3531069', - }, - { - blockNumber: '8350859', - timeStamp: '1565815221', - hash: '0x989ea9f3ee576fa43957f44363e839adf1a4a397c3d8392a4f7cbbf7949fd0ae', - nonce: '2', - blockHash: - '0xb9cf1d29c665c052e3831b5754903e539c5b0b1d33b8bcab6cd2d450764d601f', - from: '0x6bf137f335ea1b8f193b8f6ea92561a60d23a207', - contractAddress: '0x89d24a6b4ccb1b6faa2625fe562bdd9a23260359', - to: '0x09cabec1ead1c0ba254b09efb3ee13841712be14', - value: '10000000000000000000', - tokenName: 'Sai Stablecoin v1.0', - tokenSymbol: 'SAI', - tokenDecimal: '18', - transactionIndex: '31', - gas: '60734', - gasPrice: '1000000000', - gasUsed: '54745', - cumulativeGasUsed: '7833857', - input: 'deprecated', - confirmations: '3531056', - }, - { - blockNumber: '8679548', - timeStamp: '1570244087', - hash: '0xc0016b89b3b525b30d73f242653b0d80ec3ebf285376dff5bb52cef3261498b2', - nonce: '3', - blockHash: - '0x1ceb2f8b83087f010773e2acf63d1526633c8a884bd1980f118a1bba576be69f', - from: '0x6bf137f335ea1b8f193b8f6ea92561a60d23a207', - contractAddress: '0x89d24a6b4ccb1b6faa2625fe562bdd9a23260359', - to: '0xdfa6edae2ec0cf1d4a60542422724a48195a5071', - value: '0', - tokenName: 'Sai Stablecoin v1.0', - tokenSymbol: 'SAI', - tokenDecimal: '18', - transactionIndex: '56', - gas: '993379', - gasPrice: '1440000000', - gasUsed: '647253', - cumulativeGasUsed: '3562204', - input: 'deprecated', - confirmations: '3202367', - }, - { - blockNumber: '8679548', - timeStamp: '1570244087', - hash: '0xc0016b89b3b525b30d73f242653b0d80ec3ebf285376dff5bb52cef3261498b2', - nonce: '3', - blockHash: - '0x1ceb2f8b83087f010773e2acf63d1526633c8a884bd1980f118a1bba576be69f', - from: '0xdfa6edae2ec0cf1d4a60542422724a48195a5071', - contractAddress: '0x89d24a6b4ccb1b6faa2625fe562bdd9a23260359', - to: '0x6bf137f335ea1b8f193b8f6ea92561a60d23a207', - value: '0', - tokenName: 'Sai Stablecoin v1.0', - tokenSymbol: 'SAI', - tokenDecimal: '18', - transactionIndex: '56', - gas: '993379', - gasPrice: '1440000000', - gasUsed: '647253', - cumulativeGasUsed: '3562204', - input: 'deprecated', - confirmations: '3202367', - }, - { - blockNumber: '8694142', - timeStamp: '1570440625', - hash: '0xd8397138bb93d56e50d01e92a9eae99ebd3ae28844acdaa4663976a5501116cf', - nonce: '2837', - blockHash: - '0xba45dd64e71e146066af9b6d2dd3bc5d72f4a3399148c155dced74c139fc3c51', - from: '0xdfa6edae2ec0cf1d4a60542422724a48195a5071', - contractAddress: '0x89d24a6b4ccb1b6faa2625fe562bdd9a23260359', - to: '0x6bf137f335ea1b8f193b8f6ea92561a60d23a207', - value: '0', - tokenName: 'Sai Stablecoin v1.0', - tokenSymbol: 'SAI', - tokenDecimal: '18', - transactionIndex: '217', - gas: '600632', - gasPrice: '9000000000', - gasUsed: '570632', - cumulativeGasUsed: '9023725', - input: 'deprecated', - confirmations: '3187773', - }, - { - blockNumber: '10877041', - timeStamp: '1600310867', - hash: '0xc8bd16b6b41b4c24849eb6869702e1489c808cb5b125b01f084e38fefcb5ea77', - nonce: '4', - blockHash: - '0x7fa16a022bcf1f69c2d7adf6bd7d3f058e808eec5c66aaa910dfa8016a5333d1', - from: '0x090d4613473dee047c3f2706764f49e0821d256e', - contractAddress: '0x1f9840a85d5af5bf1d1762f925bdaddc4201f984', - to: '0x6bf137f335ea1b8f193b8f6ea92561a60d23a207', - value: '400000000000000000000', - tokenName: 'Uniswap', - tokenSymbol: 'UNI', - tokenDecimal: '18', - transactionIndex: '42', - gas: '90038', - gasPrice: '550000000000', - gasUsed: '81853', - cumulativeGasUsed: '3163540', - input: 'deprecated', - confirmations: '1004874', - }, - { - blockNumber: '10877897', - timeStamp: '1600321973', - hash: '0xa7162489faef826ee77862ed5210b01726524f09428f69842118dad394842d62', - nonce: '6', - blockHash: - '0xa74eb9d16f65f307dde4ce58c813c981b28f242edf1090ee2ac42caac9dccaca', - from: '0x6bf137f335ea1b8f193b8f6ea92561a60d23a207', - contractAddress: '0x1f9840a85d5af5bf1d1762f925bdaddc4201f984', - to: '0x5e736f1f25992b2cad20ded179a52823d3d24b26', - value: '400000000000000000000', - tokenName: 'Uniswap', - tokenSymbol: 'UNI', - tokenDecimal: '18', - transactionIndex: '86', - gas: '60759', - gasPrice: '640000000000', - gasUsed: '25506', - cumulativeGasUsed: '4408393', - input: 'deprecated', - confirmations: '1004018', - }, -]; - -const TRANSACTIONS_IN_STATE: TransactionMeta[] = [ - // Token tx, hash is in TOKEN_TRANSACTIONS - { - id: 'token-transaction-id', - chainId: '1', - status: TransactionStatus.confirmed, - time: 1615497996125, - transaction: { - from: '0x6bf137f335ea1b8f193b8f6ea92561a60d23a207', - data: '0x', - gas: '0x6f4d2', - gasPrice: '0x2b12dbfa00', - nonce: '0x12', - to: '0x881d40237659c251811cec9c364ef91dc08d300c', - value: '0x0', - }, - transactionHash: TOKEN_TRANSACTION_HASH, - toSmartContract: true, - }, - // ETH tx, hash is in ETH_TRANSACTIONS - { - id: 'eth-transaction-id', - chainId: '1', - status: TransactionStatus.confirmed, - time: 1615497996125, - transaction: { - from: '0x6bf137f335ea1b8f193b8f6ea92561a60d23a207', - data: '0x', - gas: '0x6f4d2', - gasPrice: '0x2b12dbfa00', - nonce: '0x12', - to: '0x6bf137f335ea1b8f193b8f6ea92561a60d23a207', - value: '100000000000000000', - }, - transactionHash: ETHER_TRANSACTION_HASH, - toSmartContract: false, - }, -]; +const ETH_TRANSACTIONS = ethTxsMock(ETHER_TRANSACTION_HASH); + +const TOKEN_TRANSACTIONS = tokenTxsMock(TOKEN_TRANSACTION_HASH); + +const TRANSACTIONS_IN_STATE: TransactionMeta[] = txsInStateMock( + ETHER_TRANSACTION_HASH, + TOKEN_TRANSACTION_HASH, +); + +const TRANSACTIONS_IN_STATE_WITH_OUTDATED_STATUS: TransactionMeta[] = txsInStateWithOutdatedStatusMock( + ETHER_TRANSACTION_HASH, + TOKEN_TRANSACTION_HASH, +); + +const TRANSACTIONS_IN_STATE_WITH_OUTDATED_GAS_DATA: TransactionMeta[] = txsInStateWithOutdatedGasDataMock( + ETHER_TRANSACTION_HASH, + TOKEN_TRANSACTION_HASH, +); + +const TRANSACTIONS_IN_STATE_WITH_OUTDATED_STATUS_AND_GAS_DATA: TransactionMeta[] = txsInStateWithOutdatedStatusAndGasDataMock( + ETHER_TRANSACTION_HASH, + TOKEN_TRANSACTION_HASH, +); const ETH_TX_HISTORY_DATA = { message: 'OK', @@ -579,12 +205,14 @@ const ETH_TX_HISTORY_DATA_ROPSTEN_NO_TRANSACTIONS_FOUND = { }; const MOCK_FETCH_TX_HISTORY_DATA_OK = { - 'https://api-ropsten.etherscan.io/api?module=account&action=tokentx&address=0x6bf137f335ea1b8f193b8f6ea92561a60d23a207&tag=latest&page=1': ETH_TX_HISTORY_DATA_ROPSTEN_NO_TRANSACTIONS_FOUND, - 'https://api.etherscan.io/api?module=account&action=tokentx&address=0x6bf137f335ea1b8f193b8f6ea92561a60d23a207&tag=latest&page=1': TOKEN_TX_HISTORY_DATA, - 'https://api.etherscan.io/api?module=account&action=tokentx&address=0x6bf137f335ea1b8f193b8f6ea92561a60d23a207&tag=latest&page=1&startBlock=999': TOKEN_TX_HISTORY_DATA_FROM_BLOCK, - 'https://api.etherscan.io/api?module=account&action=txlist&address=0x6bf137f335ea1b8f193b8f6ea92561a60d23a207&tag=latest&page=1': ETH_TX_HISTORY_DATA, - 'https://api-ropsten.etherscan.io/api?module=account&action=txlist&address=0x6bf137f335ea1b8f193b8f6ea92561a60d23a207&tag=latest&page=1': ETH_TX_HISTORY_DATA, - 'https://api.etherscan.io/api?module=account&action=txlist&address=0x6bf137f335ea1b8f193b8f6ea92561a60d23a207&tag=latest&page=1&startBlock=999': ETH_TX_HISTORY_DATA_FROM_BLOCK, + 'https://api-ropsten.etherscan.io/api?module=account&address=0x6bf137f335ea1b8f193b8f6ea92561a60d23a207&offset=40&order=desc&action=tokentx&tag=latest&page=1': ETH_TX_HISTORY_DATA_ROPSTEN_NO_TRANSACTIONS_FOUND, + 'https://api.etherscan.io/api?module=account&address=0x6bf137f335ea1b8f193b8f6ea92561a60d23a207&offset=40&order=desc&action=tokentx&tag=latest&page=1': TOKEN_TX_HISTORY_DATA, + 'https://api.etherscan.io/api?module=account&address=0x6bf137f335ea1b8f193b8f6ea92561a60d23a207&startBlock=999&offset=40&order=desc&action=tokentx&tag=latest&page=1': TOKEN_TX_HISTORY_DATA_FROM_BLOCK, + 'https://api.etherscan.io/api?module=account&address=0x6bf137f335ea1b8f193b8f6ea92561a60d23a207&offset=40&order=desc&action=txlist&tag=latest&page=1': ETH_TX_HISTORY_DATA, + 'https://api-ropsten.etherscan.io/api?module=account&address=0x6bf137f335ea1b8f193b8f6ea92561a60d23a207&offset=40&order=desc&action=txlist&tag=latest&page=1': ETH_TX_HISTORY_DATA, + 'https://api.etherscan.io/api?module=account&address=0x6bf137f335ea1b8f193b8f6ea92561a60d23a207&startBlock=999&offset=40&order=desc&action=txlist&tag=latest&page=1': ETH_TX_HISTORY_DATA_FROM_BLOCK, + 'https://api-ropsten.etherscan.io/api?module=account&address=0x6bf137f335ea1b8f193b8f6ea92561a60d23a207&offset=2&order=desc&action=tokentx&tag=latest&page=1': ETH_TX_HISTORY_DATA_ROPSTEN_NO_TRANSACTIONS_FOUND, + 'https://api-ropsten.etherscan.io/api?module=account&address=0x6bf137f335ea1b8f193b8f6ea92561a60d23a207&offset=2&order=desc&action=txlist&tag=latest&page=1': ETH_TX_HISTORY_DATA, }; const MOCK_FETCH_TX_HISTORY_DATA_ERROR = { @@ -1112,7 +740,7 @@ describe('TransactionController', () => { expect(controller.state.transactions[0].transaction.to).toBe(from); }); - it('should fetch all the transactions from an address, including incoming token transactions, but not adding the ones already in state', async () => { + it('should fetch all the transactions from an address, including incoming token transactions without modifying transactions that have the same data in local and remote', async () => { globalAny.fetch = mockFetchs(MOCK_FETCH_TX_HISTORY_DATA_OK); const controller = new TransactionController({ getNetworkState: () => MOCK_MAINNET_NETWORK.state, @@ -1133,7 +761,6 @@ describe('TransactionController', () => { expect(tokenTransaction?.id).toStrictEqual('token-transaction-id'); expect(ethTransaction?.id).toStrictEqual('eth-transaction-id'); }); - it('should fetch all the transactions from an address, including incoming transactions, in mainnet from block', async () => { globalAny.fetch = mockFetchs(MOCK_FETCH_TX_HISTORY_DATA_OK); const controller = new TransactionController({ @@ -1150,7 +777,83 @@ describe('TransactionController', () => { expect(latestBlock).toBe('4535101'); expect(controller.state.transactions[0].transaction.to).toBe(from); }); + it('should fetch and updated all transactions with outdated status regarding the data provided by the remote source in mainnet', async () => { + globalAny.fetch = mockFetchs(MOCK_FETCH_TX_HISTORY_DATA_OK); + const controller = new TransactionController({ + getNetworkState: () => MOCK_MAINNET_NETWORK.state, + onNetworkStateChange: MOCK_MAINNET_NETWORK.subscribe, + getProvider: MOCK_MAINNET_NETWORK.getProvider, + }); + const from = '0x6bf137f335ea1b8f193b8f6ea92561a60d23a207'; + controller.wipeTransactions(); + expect(controller.state.transactions).toHaveLength(0); + + controller.state.transactions = TRANSACTIONS_IN_STATE_WITH_OUTDATED_STATUS; + await controller.fetchAll(from); + expect(controller.state.transactions).toHaveLength(17); + + const tokenTransaction = controller.state.transactions.find( + ({ transactionHash }) => transactionHash === TOKEN_TRANSACTION_HASH, + ) || { status: TransactionStatus.failed }; + const ethTransaction = controller.state.transactions.find( + ({ transactionHash }) => transactionHash === ETHER_TRANSACTION_HASH, + ) || { status: TransactionStatus.failed }; + expect(tokenTransaction?.status).toStrictEqual(TransactionStatus.confirmed); + expect(ethTransaction?.status).toStrictEqual(TransactionStatus.confirmed); + }); + it('should fetch and updated all transactions with outdated gas data regarding the data provided by the remote source in mainnet', async () => { + globalAny.fetch = mockFetchs(MOCK_FETCH_TX_HISTORY_DATA_OK); + const controller = new TransactionController({ + getNetworkState: () => MOCK_MAINNET_NETWORK.state, + onNetworkStateChange: MOCK_MAINNET_NETWORK.subscribe, + getProvider: MOCK_MAINNET_NETWORK.getProvider, + }); + const from = '0x6bf137f335ea1b8f193b8f6ea92561a60d23a207'; + controller.wipeTransactions(); + expect(controller.state.transactions).toHaveLength(0); + + controller.state.transactions = TRANSACTIONS_IN_STATE_WITH_OUTDATED_GAS_DATA; + + await controller.fetchAll(from); + expect(controller.state.transactions).toHaveLength(17); + + const tokenTransaction = controller.state.transactions.find( + ({ transactionHash }) => transactionHash === TOKEN_TRANSACTION_HASH, + ) || { transaction: { gasUsed: '0' } }; + const ethTransaction = controller.state.transactions.find( + ({ transactionHash }) => transactionHash === ETHER_TRANSACTION_HASH, + ) || { transaction: { gasUsed: '0x0' } }; + expect(tokenTransaction?.transaction.gasUsed).toStrictEqual('21000'); + expect(ethTransaction?.transaction.gasUsed).toStrictEqual('0x5208'); + }); + it('should fetch and updated all transactions with outdated status and gas data regarding the data provided by the remote source in mainnet', async () => { + globalAny.fetch = mockFetchs(MOCK_FETCH_TX_HISTORY_DATA_OK); + const controller = new TransactionController({ + getNetworkState: () => MOCK_MAINNET_NETWORK.state, + onNetworkStateChange: MOCK_MAINNET_NETWORK.subscribe, + getProvider: MOCK_MAINNET_NETWORK.getProvider, + }); + const from = '0x6bf137f335ea1b8f193b8f6ea92561a60d23a207'; + controller.wipeTransactions(); + expect(controller.state.transactions).toHaveLength(0); + + controller.state.transactions = TRANSACTIONS_IN_STATE_WITH_OUTDATED_STATUS_AND_GAS_DATA; + + await controller.fetchAll(from); + expect(controller.state.transactions).toHaveLength(17); + + const tokenTransaction = controller.state.transactions.find( + ({ transactionHash }) => transactionHash === TOKEN_TRANSACTION_HASH, + ) || { status: TransactionStatus.failed, transaction: { gasUsed: '0' } }; + const ethTransaction = controller.state.transactions.find( + ({ transactionHash }) => transactionHash === ETHER_TRANSACTION_HASH, + ) || { status: TransactionStatus.failed, transaction: { gasUsed: '0x0' } }; + expect(tokenTransaction?.status).toStrictEqual(TransactionStatus.confirmed); + expect(ethTransaction?.status).toStrictEqual(TransactionStatus.confirmed); + expect(tokenTransaction?.transaction.gasUsed).toStrictEqual('21000'); + expect(ethTransaction?.transaction.gasUsed).toStrictEqual('0x5208'); + }); it('should return', async () => { globalAny.fetch = mockFetch(MOCK_FETCH_TX_HISTORY_DATA_ERROR); const controller = new TransactionController({ @@ -1299,7 +1002,6 @@ describe('TransactionController', () => { expect(controller.state.transactions[0].transaction.gasPrice).toBe( '0x4a817c800', ); - console.log(controller.state.transactions); resolve(''); }); }); diff --git a/src/transaction/TransactionController.ts b/src/transaction/TransactionController.ts index 1838a3d0ef..a17aa8c20e 100644 --- a/src/transaction/TransactionController.ts +++ b/src/transaction/TransactionController.ts @@ -65,6 +65,7 @@ export interface FetchAllOptions { * @property from - Address to send this transaction from * @property gas - Gas to send with this transaction * @property gasPrice - Price of gas with this transaction + * @property gasUsed - Gas used in the transaction * @property nonce - Unique number to prevent replay attacks * @property to - Address to send this transaction to * @property value - Value associated with this transaction @@ -75,6 +76,7 @@ export interface Transaction { from: string; gas?: string; gasPrice?: string; + gasUsed?: string; nonce?: string; to?: string; value?: string; @@ -117,6 +119,14 @@ export enum WalletDevice { OTHER = 'other_device', } +/** + * Source of data used to reconcile local transactions end state + */ +export enum StateReconcileMethod { + ETHERSCAN = 'etherscan', + BLOCKCHAIN = 'blockchain', +} + type TransactionMetaBase = { isTransfer?: boolean; transferInformation?: { @@ -316,6 +326,7 @@ export class TransactionController extends BaseController< from: txMeta.from, gas: BNToHex(new BN(txMeta.gas)), gasPrice: BNToHex(new BN(txMeta.gasPrice)), + gasUsed: BNToHex(new BN(txMeta.gasUsed)), nonce: BNToHex(new BN(txMeta.nonce)), to: txMeta.to, value: BNToHex(new BN(txMeta.value)), @@ -350,6 +361,7 @@ export class TransactionController extends BaseController< from, gas, gasPrice, + gasUsed, hash, contractAddress, tokenDecimal, @@ -368,6 +380,7 @@ export class TransactionController extends BaseController< from, gas, gasPrice, + gasUsed, to, value, }, @@ -517,7 +530,7 @@ export class TransactionController extends BaseController< try { const { gas } = await this.estimateGas(transaction); transaction.gas = gas; - } catch (error) { + } catch (error: any) { this.failTransaction(transactionMeta, error); return Promise.reject(error); } @@ -682,7 +695,7 @@ export class TransactionController extends BaseController< transactionMeta.status = TransactionStatus.submitted; this.updateTransaction(transactionMeta); this.hub.emit(`${transactionMeta.id}:finished`, transactionMeta); - } catch (error) { + } catch (error: any) { this.failTransaction(transactionMeta, error); } finally { releaseLock(); @@ -1017,7 +1030,8 @@ export class TransactionController extends BaseController< await safelyExecute(() => Promise.all( transactions.map(async (meta, index) => { - // Using fallback to networkID only when there is no chainId present. Should be removed when networkID is completely removed. + // Using fallback to networkID only when there is no chainId present. + // Should be removed when networkID is completely removed. if ( meta.status === TransactionStatus.submitted && (meta.chainId === currentChainId || @@ -1026,6 +1040,12 @@ export class TransactionController extends BaseController< const txObj = await query(this.ethQuery, 'getTransactionByHash', [ meta.transactionHash, ]); + + if (txObj === null) { + transactions[index].status = TransactionStatus.failed; + gotUpdates = true; + } + /* istanbul ignore next */ if (txObj?.blockNumber) { transactions[index].status = TransactionStatus.confirmed; @@ -1102,6 +1122,7 @@ export class TransactionController extends BaseController< ): Promise { const { provider, network: currentNetworkID } = this.getNetworkState(); const { chainId: currentChainId, type: networkType } = provider; + const { transactions } = this.state; const supportedNetworkIds = ['1', '3', '4', '42']; /* istanbul ignore next */ @@ -1112,7 +1133,12 @@ export class TransactionController extends BaseController< const [ etherscanTxResponse, etherscanTokenResponse, - ] = await handleTransactionFetch(networkType, address, opt); + ] = await handleTransactionFetch( + networkType, + address, + this.config.txHistoryLimit, + opt, + ); const normalizedTxs = etherscanTxResponse.result.map( (tx: EtherscanTransactionMeta) => @@ -1123,14 +1149,12 @@ export class TransactionController extends BaseController< this.normalizeTokenTx(tx, currentNetworkID, currentChainId), ); - const remoteTxs = [...normalizedTxs, ...normalizedTokenTxs].filter((tx) => { - const alreadyInTransactions = this.state.transactions.find( - ({ transactionHash }) => transactionHash === tx.transactionHash, - ); - return !alreadyInTransactions; - }); + const [updateRequired, allTxs] = this.transactionStateReconciler( + [...normalizedTxs, ...normalizedTokenTxs], + transactions, + StateReconcileMethod.ETHERSCAN, + ); - const allTxs = [...remoteTxs, ...this.state.transactions]; allTxs.sort((a, b) => (a.time < b.time ? -1 : 1)); let latestIncomingTxBlockNumber: string | undefined; @@ -1168,8 +1192,9 @@ export class TransactionController extends BaseController< } } }); - // Update state only if new transactions were fetched - if (allTxs.length > this.state.transactions.length) { + // Update state only if new transactions were fetched or + // the status or gas data of a transaction has changed + if (updateRequired) { this.update({ transactions: this.trimTransactionsForState(allTxs) }); } return latestIncomingTxBlockNumber; @@ -1186,8 +1211,11 @@ export class TransactionController extends BaseController< * nonce, same day and network combo can result in confusing or broken experiences * in the UI. The transactions are then updated using the BaseController update. * @param transactions - arrray of transactions to be applied to the state + * @returns Array of TransactionMeta with the desired length. */ - private trimTransactionsForState(transactions: TransactionMeta[]) { + private trimTransactionsForState( + transactions: TransactionMeta[], + ): TransactionMeta[] { const nonceNetworkSet = new Set(); const txsToKeep = transactions.reverse().filter((tx) => { const { chainId, networkID, status, transaction, time } = tx; @@ -1212,11 +1240,31 @@ export class TransactionController extends BaseController< } /** - * Fucntion to determine if the transaction is in a final state + * Resolves the locally stored transactions with the blockchain or etherscan to update TransactionController State + * @param remoteTxs - Array of transactions fetched from etherscan, the blockchain or other source + * @param localTxs - Array of transactions currently stored in the state of the controller + * @param stateReconcileMethod - Strategy used to reconcile the transactions + * @returns [boolean, TransactionMeta[]] + */ + private transactionStateReconciler( + remoteTxs: TransactionMeta[], + localTxs: TransactionMeta[], + stateReconcileMethod: StateReconcileMethod, + ): [boolean, TransactionMeta[]] { + switch (stateReconcileMethod) { + case StateReconcileMethod.ETHERSCAN: + return this.etherscanTransactionStateReconciler(remoteTxs, localTxs); + default: + return [false, []]; + } + } + + /** + * Method to determine if the transaction is in a final state * @param status - Transaction status * @returns boolean if the transaction is in a final state */ - private isFinalState(status: TransactionStatus) { + private isFinalState(status: TransactionStatus): boolean { return ( status === TransactionStatus.rejected || status === TransactionStatus.confirmed || @@ -1224,6 +1272,127 @@ export class TransactionController extends BaseController< status === TransactionStatus.cancelled ); } + + private etherscanTransactionStateReconciler( + remoteTxs: TransactionMeta[], + localTxs: TransactionMeta[], + ): [boolean, TransactionMeta[]] { + const updatedTxs: TransactionMeta[] = this.getUpdatedTransactions( + remoteTxs, + localTxs, + ); + + const newTxs: TransactionMeta[] = this.getNewTransactions( + remoteTxs, + localTxs, + ); + + const updatedLocalTxs = localTxs.map((tx: TransactionMeta) => { + const txIdx = updatedTxs.findIndex( + ({ transactionHash }) => transactionHash === tx.transactionHash, + ); + return txIdx === -1 ? tx : updatedTxs[txIdx]; + }); + + const updateRequired = newTxs.length > 0 || updatedLocalTxs.length > 0; + + return [updateRequired, [...newTxs, ...updatedLocalTxs]]; + } + + /** + * Get all transactions that are in the remote transactions array + * but not in the local transactions array + * @param remoteTxs - Array of transactions from remote source + * @param localTxs - Array of transactions stored locally + * @returns TransactionMeta array + */ + private getNewTransactions( + remoteTxs: TransactionMeta[], + localTxs: TransactionMeta[], + ): TransactionMeta[] { + return remoteTxs.filter((tx) => { + const alreadyInTransactions = localTxs.find( + ({ transactionHash }) => transactionHash === tx.transactionHash, + ); + return !alreadyInTransactions; + }); + } + + /** + * Get all the transactions that are locally outdated with respect + * to a remote source (etherscan or blockchain). The returned array + * contains the transactions with the updated data. + * @param remoteTxs - Array of transactions from remote source + * @param localTxs - Array of transactions stored locally + * @returns TransactionMeta array + */ + private getUpdatedTransactions( + remoteTxs: TransactionMeta[], + localTxs: TransactionMeta[], + ): TransactionMeta[] { + return remoteTxs.filter((remoteTx) => { + const isTxOutdated = localTxs.find((localTx) => { + return ( + remoteTx.transactionHash === localTx.transactionHash && + this.isTransactionOutdated(remoteTx, localTx) + ); + }); + return isTxOutdated; + }); + } + + /** + * Verifies if a local transaction is outdated with respect to the remote transaction + * @param remoteTx - Remote transaction from Etherscan + * @param localTx - Local transaction + * @returns boolean + */ + private isTransactionOutdated( + remoteTx: TransactionMeta, + localTx: TransactionMeta, + ): boolean { + const statusOutdated = this.isStatusOutdated( + remoteTx.transactionHash, + localTx.transactionHash, + remoteTx.status, + localTx.status, + ); + const gasDataOutdated = this.isGasDataOutdated( + remoteTx.transaction.gasUsed, + localTx.transaction.gasUsed, + ); + return statusOutdated || gasDataOutdated; + } + + /** + * Verifies if the status of a local transaction is outdated with respect to the remote transaction + * @param remoteTxHash - Remote transaction hash + * @param localTxHash - Local transaction hash + * @param remoteTxStatus - Remote transaction status + * @param localTxStatus - Local transaction status + * @returns boolean + */ + private isStatusOutdated( + remoteTxHash: string | undefined, + localTxHash: string | undefined, + remoteTxStatus: TransactionStatus, + localTxStatus: TransactionStatus, + ): boolean { + return remoteTxHash === localTxHash && remoteTxStatus !== localTxStatus; + } + + /** + * Verifies if the gas data of a local transaction is outdated with respect to the remote transaction + * @param remoteGasUsed - Remote gas used in the transaction + * @param localGasUsed - Local gas used in the transaction + * @returns boolean + */ + private isGasDataOutdated( + remoteGasUsed: string | undefined, + localGasUsed: string | undefined, + ): boolean { + return remoteGasUsed !== localGasUsed; + } } export default TransactionController; diff --git a/src/transaction/mocks/txsMock.ts b/src/transaction/mocks/txsMock.ts new file mode 100644 index 0000000000..61b435f1a0 --- /dev/null +++ b/src/transaction/mocks/txsMock.ts @@ -0,0 +1,536 @@ +import { TransactionMeta, TransactionStatus } from '../TransactionController'; + +export const ethTxsMock = (ethTxHash: string) => [ + { + blockNumber: '4535101', + confirmations: '10', + contractAddress: '', + cumulativeGasUsed: '120607', + from: '0xe46abaf75cfbff815c0b7ffed6f02b0760ea27f1', + gas: '335208', + gasPrice: '10000000000', + gasUsed: '21000', + hash: ethTxHash, + input: '0x', + isError: '0', + nonce: '9', + timeStamp: '1543596286', + to: '0x6bf137f335ea1b8f193b8f6ea92561a60d23a207', + transactionIndex: '2', + txreceipt_status: '1', + value: '100000000000000000', + }, + { + blockNumber: '4535108', + confirmations: '3', + contractAddress: '', + cumulativeGasUsed: '693910', + from: '0x6bf137f335ea1b8f193b8f6ea92561a60d23a207', + gas: '335208', + gasPrice: '20000000000', + gasUsed: '21000', + hash: '0x342e9d73e10004af41d04973339fc7219dbadcbb5629730cfe65e9f9cb15ff92', + input: '0x', + isError: '0', + nonce: '0', + timeStamp: '1543596378', + to: '0xb2d191b6fe03c5b8a1ab249cfe88c37553357a23', + transactionIndex: '12', + txreceipt_status: '1', + value: '50000000000000000', + }, + { + blockNumber: '4535105', + confirmations: '4', + contractAddress: '', + cumulativeGasUsed: '693910', + from: '0x6bf137f335ea1b8f193b8f6ea92561a60d23a207', + gas: '335208', + gasPrice: '20000000000', + gasUsed: '21000', + hash: '0x342e9d73e10004af41d04973339fc7219dbadcbb5629730cfe65e9f9cb15ff91', + input: '0x', + isError: '0', + nonce: '1', + timeStamp: '1543596356', + transactionIndex: '13', + txreceipt_status: '1', + value: '50000000000000000', + }, + { + blockNumber: '4535106', + confirmations: '4', + contractAddress: '', + cumulativeGasUsed: '693910', + from: '0x6bf137f335ea1b8f193b8f6ea92561a60d23a207', + gas: '335208', + gasPrice: '20000000000', + gasUsed: '21000', + hash: '0x342e9d73e10004af41d04973139fc7219dbadcbb5629730cfe65e9f9cb15ff91', + input: '0x11', + isError: '0', + nonce: '3', + timeStamp: '1543596356', + to: '0xb2d191b6fe03c5b8a1ab249cfe88c37553357a23', + transactionIndex: '13', + txreceipt_status: '1', + value: '50000000000000000', + }, +]; + +export const tokenTxsMock = (tokenTxHash: string) => [ + { + blockNumber: '8222239', + timeStamp: '1564091067', + hash: tokenTxHash, + nonce: '2329', + blockHash: + '0x3c30a9be9aea7be13caad419444140c11839d72e70479ec7e9c6d8bd08c533bc', + from: '0xdfa6edae2ec0cf1d4a60542422724a48195a5071', + contractAddress: '0x89d24a6b4ccb1b6faa2625fe562bdd9a23260359', + to: '0x6bf137f335ea1b8f193b8f6ea92561a60d23a207', + value: '0', + tokenName: 'Sai Stablecoin v1.0', + tokenSymbol: 'SAI', + tokenDecimal: '18', + transactionIndex: '69', + gas: '624874', + gasPrice: '20000000000', + gasUsed: '21000', + cumulativeGasUsed: '3203881', + input: 'deprecated', + confirmations: '3659676', + }, + { + blockNumber: '8222250', + timeStamp: '1564091247', + hash: '0xdcd1c8bee545d3f76d80b20a23ad44276ba2e376681228eb4570cf3518491279', + nonce: '2330', + blockHash: + '0x16986dd66bedb20a5b846ec2b6c0ecaa62f1c4b51fac58c1326101fd9126dd82', + from: '0xdfa6edae2ec0cf1d4a60542422724a48195a5071', + contractAddress: '0x89d24a6b4ccb1b6faa2625fe562bdd9a23260359', + to: '0x6bf137f335ea1b8f193b8f6ea92561a60d23a207', + value: '0', + tokenName: 'Sai Stablecoin v1.0', + tokenSymbol: 'SAI', + tokenDecimal: '18', + transactionIndex: '40', + gas: '594268', + gasPrice: '20000000000', + gasUsed: '579268', + cumulativeGasUsed: '2009011', + input: 'deprecated', + confirmations: '3659665', + }, + { + blockNumber: '8223771', + timeStamp: '1564111652', + hash: '0x070369e6f560b0deca52e050ff1a961fa7b688bbec5cea08435921c9d9b0f52e', + nonce: '2333', + blockHash: + '0x0aff8b36881be99df6d176d7c64c2171672c0483684a10c112d2c90fefe30a0a', + from: '0xdfa6edae2ec0cf1d4a60542422724a48195a5071', + contractAddress: '0x89d24a6b4ccb1b6faa2625fe562bdd9a23260359', + to: '0x6bf137f335ea1b8f193b8f6ea92561a60d23a207', + value: '0', + tokenName: 'Sai Stablecoin v1.0', + tokenSymbol: 'SAI', + tokenDecimal: '18', + transactionIndex: '132', + gas: '583810', + gasPrice: '6000000000', + gasUsed: '568810', + cumulativeGasUsed: '6956245', + input: 'deprecated', + confirmations: '3658144', + }, + { + blockNumber: '8224850', + timeStamp: '1564126442', + hash: '0x8ef20ec9597c8c2e945bcc76d2668e5d3bb088b081fe8c5b5af2e1cbd315a20f', + nonce: '31', + blockHash: + '0xb80d4d861ecb7a3cb14e591c0aaeb226842d0267772affa2acc1a590c7535647', + from: '0x6c70e3563cef0c6835703bb2664c9f59a92353e4', + contractAddress: '0x89d24a6b4ccb1b6faa2625fe562bdd9a23260359', + to: '0x6bf137f335ea1b8f193b8f6ea92561a60d23a207', + value: '10000000000000000000', + tokenName: 'Sai Stablecoin v1.0', + tokenSymbol: 'SAI', + tokenDecimal: '18', + transactionIndex: '169', + gas: '78447', + gasPrice: '2000000000', + gasUsed: '52298', + cumulativeGasUsed: '7047823', + input: 'deprecated', + confirmations: '3657065', + }, + { + blockNumber: '8228053', + timeStamp: '1564168901', + hash: '0xa0f2d7b558bb3cc28fa568f6feb8ed30eb28a01a674d7c0d4ae603fc691e6020', + nonce: '2368', + blockHash: + '0x62c515ea049842c968ca67499f47a32a11394364d319d9c9cc0a0211652a7294', + from: '0xdfa6edae2ec0cf1d4a60542422724a48195a5071', + contractAddress: '0x89d24a6b4ccb1b6faa2625fe562bdd9a23260359', + to: '0x6bf137f335ea1b8f193b8f6ea92561a60d23a207', + value: '0', + tokenName: 'Sai Stablecoin v1.0', + tokenSymbol: 'SAI', + tokenDecimal: '18', + transactionIndex: '43', + gas: '567156', + gasPrice: '3000000000', + gasUsed: '552156', + cumulativeGasUsed: '3048261', + input: 'deprecated', + confirmations: '3653862', + }, + { + blockNumber: '8315335', + timeStamp: '1565339223', + hash: '0x464df60fe00b6dd04c9e8ab341d02af9b10a619d2fcd60fd2971f10edf12118f', + nonce: '206760', + blockHash: + '0x98275388ef6708debe35ac7bf2e30143c9b1fd9e0e457ca03598fc1f4209e273', + from: '0x00cfbbaf7ddb3a1476767101c12a0162e241fbad', + contractAddress: '0x4dc3643dbc642b72c158e7f3d2ff232df61cb6ce', + to: '0x6bf137f335ea1b8f193b8f6ea92561a60d23a207', + value: '100000000000000000', + tokenName: 'Amber', + tokenSymbol: 'AMB', + tokenDecimal: '18', + transactionIndex: '186', + gas: '60000', + gasPrice: '2000000000', + gasUsed: '52108', + cumulativeGasUsed: '7490707', + input: 'deprecated', + confirmations: '3566580', + }, + { + blockNumber: '8350846', + timeStamp: '1565815049', + hash: '0xc0682327ad3efd56dfa33e8206b4e09efad4e419a6191076069d217e3ee2341f', + nonce: '2506', + blockHash: + '0xd0aa3c0e319fdfeb21b0192cf77b9760b8668060a5977a5f10f8413531083afa', + from: '0xdfa6edae2ec0cf1d4a60542422724a48195a5071', + contractAddress: '0x89d24a6b4ccb1b6faa2625fe562bdd9a23260359', + to: '0x6bf137f335ea1b8f193b8f6ea92561a60d23a207', + value: '4', + tokenName: 'Sai Stablecoin v1.0', + tokenSymbol: 'SAI', + tokenDecimal: '18', + transactionIndex: '48', + gas: '578737', + gasPrice: '3000000000', + gasUsed: '518737', + cumulativeGasUsed: '2848015', + input: 'deprecated', + confirmations: '3531069', + }, + { + blockNumber: '8350859', + timeStamp: '1565815221', + hash: '0x989ea9f3ee576fa43957f44363e839adf1a4a397c3d8392a4f7cbbf7949fd0ae', + nonce: '2', + blockHash: + '0xb9cf1d29c665c052e3831b5754903e539c5b0b1d33b8bcab6cd2d450764d601f', + from: '0x6bf137f335ea1b8f193b8f6ea92561a60d23a207', + contractAddress: '0x89d24a6b4ccb1b6faa2625fe562bdd9a23260359', + to: '0x09cabec1ead1c0ba254b09efb3ee13841712be14', + value: '10000000000000000000', + tokenName: 'Sai Stablecoin v1.0', + tokenSymbol: 'SAI', + tokenDecimal: '18', + transactionIndex: '31', + gas: '60734', + gasPrice: '1000000000', + gasUsed: '54745', + cumulativeGasUsed: '7833857', + input: 'deprecated', + confirmations: '3531056', + }, + { + blockNumber: '8679548', + timeStamp: '1570244087', + hash: '0xc0016b89b3b525b30d73f242653b0d80ec3ebf285376dff5bb52cef3261498b2', + nonce: '3', + blockHash: + '0x1ceb2f8b83087f010773e2acf63d1526633c8a884bd1980f118a1bba576be69f', + from: '0x6bf137f335ea1b8f193b8f6ea92561a60d23a207', + contractAddress: '0x89d24a6b4ccb1b6faa2625fe562bdd9a23260359', + to: '0xdfa6edae2ec0cf1d4a60542422724a48195a5071', + value: '0', + tokenName: 'Sai Stablecoin v1.0', + tokenSymbol: 'SAI', + tokenDecimal: '18', + transactionIndex: '56', + gas: '993379', + gasPrice: '1440000000', + gasUsed: '647253', + cumulativeGasUsed: '3562204', + input: 'deprecated', + confirmations: '3202367', + }, + { + blockNumber: '8679548', + timeStamp: '1570244087', + hash: '0xc0016b89b3b525b30d73f242653b0d80ec3ebf285376dff5bb52cef3261498b2', + nonce: '3', + blockHash: + '0x1ceb2f8b83087f010773e2acf63d1526633c8a884bd1980f118a1bba576be69f', + from: '0xdfa6edae2ec0cf1d4a60542422724a48195a5071', + contractAddress: '0x89d24a6b4ccb1b6faa2625fe562bdd9a23260359', + to: '0x6bf137f335ea1b8f193b8f6ea92561a60d23a207', + value: '0', + tokenName: 'Sai Stablecoin v1.0', + tokenSymbol: 'SAI', + tokenDecimal: '18', + transactionIndex: '56', + gas: '993379', + gasPrice: '1440000000', + gasUsed: '647253', + cumulativeGasUsed: '3562204', + input: 'deprecated', + confirmations: '3202367', + }, + { + blockNumber: '8694142', + timeStamp: '1570440625', + hash: '0xd8397138bb93d56e50d01e92a9eae99ebd3ae28844acdaa4663976a5501116cf', + nonce: '2837', + blockHash: + '0xba45dd64e71e146066af9b6d2dd3bc5d72f4a3399148c155dced74c139fc3c51', + from: '0xdfa6edae2ec0cf1d4a60542422724a48195a5071', + contractAddress: '0x89d24a6b4ccb1b6faa2625fe562bdd9a23260359', + to: '0x6bf137f335ea1b8f193b8f6ea92561a60d23a207', + value: '0', + tokenName: 'Sai Stablecoin v1.0', + tokenSymbol: 'SAI', + tokenDecimal: '18', + transactionIndex: '217', + gas: '600632', + gasPrice: '9000000000', + gasUsed: '570632', + cumulativeGasUsed: '9023725', + input: 'deprecated', + confirmations: '3187773', + }, + { + blockNumber: '10877041', + timeStamp: '1600310867', + hash: '0xc8bd16b6b41b4c24849eb6869702e1489c808cb5b125b01f084e38fefcb5ea77', + nonce: '4', + blockHash: + '0x7fa16a022bcf1f69c2d7adf6bd7d3f058e808eec5c66aaa910dfa8016a5333d1', + from: '0x090d4613473dee047c3f2706764f49e0821d256e', + contractAddress: '0x1f9840a85d5af5bf1d1762f925bdaddc4201f984', + to: '0x6bf137f335ea1b8f193b8f6ea92561a60d23a207', + value: '400000000000000000000', + tokenName: 'Uniswap', + tokenSymbol: 'UNI', + tokenDecimal: '18', + transactionIndex: '42', + gas: '90038', + gasPrice: '550000000000', + gasUsed: '81853', + cumulativeGasUsed: '3163540', + input: 'deprecated', + confirmations: '1004874', + }, + { + blockNumber: '10877897', + timeStamp: '1600321973', + hash: '0xa7162489faef826ee77862ed5210b01726524f09428f69842118dad394842d62', + nonce: '6', + blockHash: + '0xa74eb9d16f65f307dde4ce58c813c981b28f242edf1090ee2ac42caac9dccaca', + from: '0x6bf137f335ea1b8f193b8f6ea92561a60d23a207', + contractAddress: '0x1f9840a85d5af5bf1d1762f925bdaddc4201f984', + to: '0x5e736f1f25992b2cad20ded179a52823d3d24b26', + value: '400000000000000000000', + tokenName: 'Uniswap', + tokenSymbol: 'UNI', + tokenDecimal: '18', + transactionIndex: '86', + gas: '60759', + gasPrice: '640000000000', + gasUsed: '25506', + cumulativeGasUsed: '4408393', + input: 'deprecated', + confirmations: '1004018', + }, +]; + +export const txsInStateMock = ( + ethTxHash: string, + tokenTxHash: string, +): TransactionMeta[] => [ + { + id: 'token-transaction-id', + chainId: '1', + status: TransactionStatus.confirmed, + time: 1615497996125, + transaction: { + from: '0x6bf137f335ea1b8f193b8f6ea92561a60d23a207', + data: '0x', + gas: '624874', + gasPrice: '20000000000', + gasUsed: '21000', + nonce: '0x12', + to: '0x881d40237659c251811cec9c364ef91dc08d300c', + value: '0x0', + }, + transactionHash: tokenTxHash, + toSmartContract: true, + }, + { + id: 'eth-transaction-id', + chainId: '1', + status: TransactionStatus.confirmed, + time: 1615497996125, + transaction: { + from: '0x6bf137f335ea1b8f193b8f6ea92561a60d23a207', + data: '0x', + gas: '0x51d68', + gasPrice: '0x2540be400', + gasUsed: '0x5208', + nonce: '0x12', + to: '0x6bf137f335ea1b8f193b8f6ea92561a60d23a207', + value: '100000000000000000', + }, + transactionHash: ethTxHash, + toSmartContract: false, + }, +]; + +export const txsInStateWithOutdatedStatusMock = ( + ethTxHash: string, + tokenTxHash: string, +): TransactionMeta[] => [ + { + id: 'token-transaction-id', + chainId: '1', + status: TransactionStatus.rejected, + time: 1615497996125, + transaction: { + from: '0x6bf137f335ea1b8f193b8f6ea92561a60d23a207', + data: '0x', + gas: '624874', + gasPrice: '20000000000', + gasUsed: '21000', + nonce: '0x12', + to: '0x881d40237659c251811cec9c364ef91dc08d300c', + value: '0x0', + }, + transactionHash: tokenTxHash, + toSmartContract: true, + }, + { + id: 'eth-transaction-id', + chainId: '1', + status: TransactionStatus.rejected, + time: 1615497996125, + transaction: { + from: '0x6bf137f335ea1b8f193b8f6ea92561a60d23a207', + data: '0x', + gas: '0x51d68', + gasPrice: '0x2540be400', + gasUsed: '0x5208', + nonce: '0x12', + to: '0x6bf137f335ea1b8f193b8f6ea92561a60d23a207', + value: '100000000000000000', + }, + transactionHash: ethTxHash, + toSmartContract: false, + }, +]; + +export const txsInStateWithOutdatedGasDataMock = ( + ethTxHash: string, + tokenTxHash: string, +): TransactionMeta[] => [ + { + id: 'token-transaction-id', + chainId: '1', + status: TransactionStatus.confirmed, + time: 1615497996125, + transaction: { + from: '0x6bf137f335ea1b8f193b8f6ea92561a60d23a207', + data: '0x', + gas: '624874', + gasPrice: '20000000000', + gasUsed: undefined, + nonce: '0x12', + to: '0x881d40237659c251811cec9c364ef91dc08d300c', + value: '0x0', + }, + transactionHash: tokenTxHash, + toSmartContract: true, + }, + { + id: 'eth-transaction-id', + chainId: '1', + status: TransactionStatus.confirmed, + time: 1615497996125, + transaction: { + from: '0x6bf137f335ea1b8f193b8f6ea92561a60d23a207', + data: '0x', + gas: '0x51d68', + gasPrice: '0x2540be400', + gasUsed: undefined, + nonce: '0x12', + to: '0x6bf137f335ea1b8f193b8f6ea92561a60d23a207', + value: '100000000000000000', + }, + transactionHash: ethTxHash, + toSmartContract: false, + }, +]; + +export const txsInStateWithOutdatedStatusAndGasDataMock = ( + ethTxHash: string, + tokenTxHash: string, +): TransactionMeta[] => [ + { + id: 'token-transaction-id', + chainId: '1', + status: TransactionStatus.rejected, + time: 1615497996125, + transaction: { + from: '0x6bf137f335ea1b8f193b8f6ea92561a60d23a207', + data: '0x', + gas: '624874', + gasPrice: '20000000000', + gasUsed: undefined, + nonce: '0x12', + to: '0x881d40237659c251811cec9c364ef91dc08d300c', + value: '0x0', + }, + transactionHash: tokenTxHash, + toSmartContract: true, + }, + { + id: 'eth-transaction-id', + chainId: '1', + status: TransactionStatus.rejected, + time: 1615497996125, + transaction: { + from: '0x6bf137f335ea1b8f193b8f6ea92561a60d23a207', + data: '0x', + gas: '0x51d68', + gasPrice: '0x2540be400', + gasUsed: undefined, + nonce: '0x12', + to: '0x6bf137f335ea1b8f193b8f6ea92561a60d23a207', + value: '100000000000000000', + }, + transactionHash: ethTxHash, + toSmartContract: false, + }, +]; diff --git a/src/util.test.ts b/src/util.test.ts index c0a343a4a9..20ef68df35 100644 --- a/src/util.test.ts +++ b/src/util.test.ts @@ -330,33 +330,31 @@ describe('util', () => { const action = 'txlist'; it('should return a correctly structured url', () => { - const url = util.getEtherscanApiUrl(networkType, address, action); + const url = util.getEtherscanApiUrl(networkType, { address, action }); expect(url.indexOf(`&action=${action}`)).toBeGreaterThan(0); }); it('should return a correctly structured url with from block', () => { const fromBlock = 'xxxxxx'; - const url = util.getEtherscanApiUrl( - networkType, + const url = util.getEtherscanApiUrl(networkType, { address, action, - fromBlock, - ); + startBlock: fromBlock, + }); expect(url.indexOf(`&startBlock=${fromBlock}`)).toBeGreaterThan(0); }); it('should return a correctly structured url with testnet subdomain', () => { const ropsten = 'ropsten'; - const url = util.getEtherscanApiUrl(ropsten, address, action); + const url = util.getEtherscanApiUrl(ropsten, { address, action }); expect(url.indexOf(`https://api-${ropsten}`)).toBe(0); }); it('should return a correctly structured url with apiKey', () => { const apiKey = 'xxxxxx'; - const url = util.getEtherscanApiUrl( - networkType, + const url = util.getEtherscanApiUrl(networkType, { address, action, - 'xxxxxx', - apiKey, - ); + startBlock: 'xxxxxx', + apikey: apiKey, + }); expect(url.indexOf(`&apikey=${apiKey}`)).toBeGreaterThan(0); }); }); diff --git a/src/util.ts b/src/util.ts index db799e5db4..05ee2699ef 100644 --- a/src/util.ts +++ b/src/util.ts @@ -148,29 +148,26 @@ export function getBuyURL( * Return a URL that can be used to fetch ETH transactions * * @param networkType - Network type of desired network - * @param address - Address to get the transactions from - * @param fromBlock? - Block from which transactions are needed - * @returns - URL to fetch the transactions from + * @param urlParams - Parameters used to construct the URL + * @returns - URL to fetch the access the endpoint */ export function getEtherscanApiUrl( networkType: string, - address: string, - action: string, - fromBlock?: string, - etherscanApiKey?: string, + urlParams: any, ): string { let etherscanSubdomain = 'api'; if (networkType !== MAINNET) { etherscanSubdomain = `api-${networkType}`; } const apiUrl = `https://${etherscanSubdomain}.etherscan.io`; - let url = `${apiUrl}/api?module=account&action=${action}&address=${address}&tag=latest&page=1`; - if (fromBlock) { - url += `&startBlock=${fromBlock}`; - } - if (etherscanApiKey) { - url += `&apikey=${etherscanApiKey}`; + let url = `${apiUrl}/api?`; + + for (const paramKey in urlParams) { + if (urlParams[paramKey]) { + url += `${paramKey}=${urlParams[paramKey]}&`; + } } + url += 'tag=latest&page=1'; return url; } @@ -185,26 +182,29 @@ export function getEtherscanApiUrl( export async function handleTransactionFetch( networkType: string, address: string, + txHistoryLimit: number, opt?: FetchAllOptions, ): Promise<[{ [result: string]: [] }, { [result: string]: [] }]> { // transactions - const etherscanTxUrl = getEtherscanApiUrl( - networkType, + const urlParams = { + module: 'account', address, - 'txlist', - opt?.fromBlock, - opt?.etherscanApiKey, - ); + startBlock: opt?.fromBlock, + apikey: opt?.etherscanApiKey, + offset: txHistoryLimit.toString(), + order: 'desc', + }; + const etherscanTxUrl = getEtherscanApiUrl(networkType, { + ...urlParams, + action: 'txlist', + }); const etherscanTxResponsePromise = handleFetch(etherscanTxUrl); // tokens - const etherscanTokenUrl = getEtherscanApiUrl( - networkType, - address, - 'tokentx', - opt?.fromBlock, - opt?.etherscanApiKey, - ); + const etherscanTokenUrl = getEtherscanApiUrl(networkType, { + ...urlParams, + action: 'tokentx', + }); const etherscanTokenResponsePromise = handleFetch(etherscanTokenUrl); let [etherscanTxResponse, etherscanTokenResponse] = await Promise.all([ @@ -216,14 +216,17 @@ export async function handleTransactionFetch( etherscanTxResponse.status === '0' || etherscanTxResponse.result.length <= 0 ) { - etherscanTxResponse = { result: [] }; + etherscanTxResponse = { status: etherscanTxResponse.status, result: [] }; } if ( etherscanTokenResponse.status === '0' || etherscanTokenResponse.result.length <= 0 ) { - etherscanTokenResponse = { result: [] }; + etherscanTokenResponse = { + status: etherscanTokenResponse.status, + result: [], + }; } return [etherscanTxResponse, etherscanTokenResponse];