Implement new transaction list design (#8564)
Co-authored-by: Whymarrh Whitby <whymarrh.whitby@gmail.com> Co-authored-by: Mark Stacey <markjstacey@gmail.com>feature/default_network_editable
parent
e06fb2c9f6
commit
706dc02cb4
@ -0,0 +1,499 @@ |
||||
[ |
||||
{ |
||||
"nonce": "0xc", |
||||
"initialTransaction": { |
||||
"id": 4243712234858512, |
||||
"time": 1589314601567, |
||||
"status": "confirmed", |
||||
"metamaskNetworkId": "4", |
||||
"loadingDefaults": false, |
||||
"txParams": { |
||||
"from": "0x9eca64466f257793eaa52fcfff5066894b76a149", |
||||
"to": "0xffe5bc4e8f1f969934d773fa67da095d2e491a97", |
||||
"nonce": "0xc", |
||||
"value": "0xde0b6b3a7640000", |
||||
"gas": "0x5208", |
||||
"gasPrice": "0x2540be400" |
||||
}, |
||||
"type": "standard", |
||||
"origin": "metamask", |
||||
"transactionCategory": "sentEther", |
||||
"nonceDetails": { |
||||
"params": { |
||||
"highestLocallyConfirmed": 12, |
||||
"highestSuggested": 12, |
||||
"nextNetworkNonce": 12 |
||||
}, |
||||
"local": { |
||||
"name": "local", |
||||
"nonce": 12, |
||||
"details": { |
||||
"startPoint": 12, |
||||
"highest": 12 |
||||
} |
||||
}, |
||||
"network": { |
||||
"name": "network", |
||||
"nonce": 12, |
||||
"details": { |
||||
"blockNumber": "0x62d5dc", |
||||
"baseCount": 12 |
||||
} |
||||
} |
||||
}, |
||||
"r": "0xe0b79a8e33b15460ea79b05a5fb16bc067a796592eeb4edc5007c88615c12595", |
||||
"s": "0x1c834a25f1df07af5122996a40e99e554a40dc971a25041bc6e31638846c4f58", |
||||
"v": "0x2c", |
||||
"rawTx": "0xf86c0c8502540be40082520894ffe5bc4e8f1f969934d773fa67da095d2e491a97880de0b6b3a7640000802ca0e0b79a8e33b15460ea79b05a5fb16bc067a796592eeb4edc5007c88615c12595a01c834a25f1df07af5122996a40e99e554a40dc971a25041bc6e31638846c4f58", |
||||
"hash": "0x06bb79b856f5eb67025e4c4ffff44bca26ae135d1c3e6bd9a4193f422dcecca2", |
||||
"submittedTime": 1589314602908, |
||||
"txReceipt": { |
||||
"blockHash": "0xb9d2d71153b66146fde74b14b1c1ffc0588eb4a02ff464e32a4db9ae4bbfad8a", |
||||
"blockNumber": "62d5de", |
||||
"contractAddress": null, |
||||
"cumulativeGasUsed": "5208", |
||||
"from": "0x9eca64466f257793eaa52fcfff5066894b76a149", |
||||
"gasUsed": "5208", |
||||
"logs": [], |
||||
"logsBloom": "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", |
||||
"status": "0x1", |
||||
"to": "0xffe5bc4e8f1f969934d773fa67da095d2e491a97", |
||||
"transactionHash": "0x06bb79b856f5eb67025e4c4ffff44bca26ae135d1c3e6bd9a4193f422dcecca2", |
||||
"transactionIndex": "0" |
||||
} |
||||
}, |
||||
"primaryTransaction": { |
||||
"id": 4243712234858512, |
||||
"time": 1589314601567, |
||||
"status": "confirmed", |
||||
"metamaskNetworkId": "4", |
||||
"loadingDefaults": false, |
||||
"txParams": { |
||||
"from": "0x9eca64466f257793eaa52fcfff5066894b76a149", |
||||
"to": "0xffe5bc4e8f1f969934d773fa67da095d2e491a97", |
||||
"nonce": "0xc", |
||||
"value": "0xde0b6b3a7640000", |
||||
"gas": "0x5208", |
||||
"gasPrice": "0x2540be400" |
||||
}, |
||||
"type": "standard", |
||||
"origin": "metamask", |
||||
"transactionCategory": "sentEther", |
||||
"nonceDetails": { |
||||
"params": { |
||||
"highestLocallyConfirmed": 12, |
||||
"highestSuggested": 12, |
||||
"nextNetworkNonce": 12 |
||||
}, |
||||
"local": { |
||||
"name": "local", |
||||
"nonce": 12, |
||||
"details": { |
||||
"startPoint": 12, |
||||
"highest": 12 |
||||
} |
||||
}, |
||||
"network": { |
||||
"name": "network", |
||||
"nonce": 12, |
||||
"details": { |
||||
"blockNumber": "0x62d5dc", |
||||
"baseCount": 12 |
||||
} |
||||
} |
||||
}, |
||||
"r": "0xe0b79a8e33b15460ea79b05a5fb16bc067a796592eeb4edc5007c88615c12595", |
||||
"s": "0x1c834a25f1df07af5122996a40e99e554a40dc971a25041bc6e31638846c4f58", |
||||
"v": "0x2c", |
||||
"rawTx": "0xf86c0c8502540be40082520894ffe5bc4e8f1f969934d773fa67da095d2e491a97880de0b6b3a7640000802ca0e0b79a8e33b15460ea79b05a5fb16bc067a796592eeb4edc5007c88615c12595a01c834a25f1df07af5122996a40e99e554a40dc971a25041bc6e31638846c4f58", |
||||
"hash": "0x06bb79b856f5eb67025e4c4ffff44bca26ae135d1c3e6bd9a4193f422dcecca2", |
||||
"submittedTime": 1589314602908, |
||||
"txReceipt": { |
||||
"blockHash": "0xb9d2d71153b66146fde74b14b1c1ffc0588eb4a02ff464e32a4db9ae4bbfad8a", |
||||
"blockNumber": "62d5de", |
||||
"contractAddress": null, |
||||
"cumulativeGasUsed": "5208", |
||||
"from": "0x9eca64466f257793eaa52fcfff5066894b76a149", |
||||
"gasUsed": "5208", |
||||
"logs": [], |
||||
"logsBloom": "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", |
||||
"status": "0x1", |
||||
"to": "0xffe5bc4e8f1f969934d773fa67da095d2e491a97", |
||||
"transactionHash": "0x06bb79b856f5eb67025e4c4ffff44bca26ae135d1c3e6bd9a4193f422dcecca2", |
||||
"transactionIndex": "0" |
||||
} |
||||
}, |
||||
"hasRetried": false, |
||||
"hasCancelled": false |
||||
}, |
||||
{ |
||||
"nonce": "0xb", |
||||
"initialTransaction": { |
||||
"id": 4243712234858507, |
||||
"time": 1589314355872, |
||||
"status": "confirmed", |
||||
"metamaskNetworkId": "4", |
||||
"loadingDefaults": false, |
||||
"txParams": { |
||||
"from": "0x9eca64466f257793eaa52fcfff5066894b76a149", |
||||
"to": "0x0ccc8aeeaf5ce790f3b448325981a143fdef8848", |
||||
"nonce": "0xb", |
||||
"value": "0x1bc16d674ec80000", |
||||
"gas": "0x5208", |
||||
"gasPrice": "0x2540be400" |
||||
}, |
||||
"type": "standard", |
||||
"origin": "metamask", |
||||
"transactionCategory": "sentEther", |
||||
"nonceDetails": { |
||||
"params": { |
||||
"highestLocallyConfirmed": 0, |
||||
"highestSuggested": 10, |
||||
"nextNetworkNonce": 10 |
||||
}, |
||||
"local": { |
||||
"name": "local", |
||||
"nonce": 11, |
||||
"details": { |
||||
"startPoint": 10, |
||||
"highest": 11 |
||||
} |
||||
}, |
||||
"network": { |
||||
"name": "network", |
||||
"nonce": 10, |
||||
"details": { |
||||
"blockNumber": "0x62d5cc", |
||||
"baseCount": 10 |
||||
} |
||||
} |
||||
}, |
||||
"r": "0xe6828baea0a93a52779ffa5ea55e927781fb7d4be58107a29c75d314d433d055", |
||||
"s": "0x10613f984c57b8928d8ed9fce16ddda5746767e5f68f4c8fc29542e86a61f458", |
||||
"v": "0x2b", |
||||
"rawTx": "0xf86c0b8502540be400825208940ccc8aeeaf5ce790f3b448325981a143fdef8848881bc16d674ec80000802ba0e6828baea0a93a52779ffa5ea55e927781fb7d4be58107a29c75d314d433d055a010613f984c57b8928d8ed9fce16ddda5746767e5f68f4c8fc29542e86a61f458", |
||||
"hash": "0x2ccb9e2c0c64399ebc5c4ac70bebc5b537248458dee6cbce32df4b50c9e73bbd", |
||||
"submittedTime": 1589314356907, |
||||
"txReceipt": { |
||||
"blockHash": "0xfa3c8b63aaba2ef64ab328af72811dd5110a7641bd435cc6fbdfd9ea0d334542", |
||||
"blockNumber": "62d5ce", |
||||
"contractAddress": null, |
||||
"cumulativeGasUsed": "5208", |
||||
"from": "0x9eca64466f257793eaa52fcfff5066894b76a149", |
||||
"gasUsed": "5208", |
||||
"logs": [], |
||||
"logsBloom": "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", |
||||
"status": "0x1", |
||||
"to": "0x0ccc8aeeaf5ce790f3b448325981a143fdef8848", |
||||
"transactionHash": "0x2ccb9e2c0c64399ebc5c4ac70bebc5b537248458dee6cbce32df4b50c9e73bbd", |
||||
"transactionIndex": "0" |
||||
} |
||||
}, |
||||
"primaryTransaction": { |
||||
"id": 4243712234858507, |
||||
"time": 1589314355872, |
||||
"status": "confirmed", |
||||
"metamaskNetworkId": "4", |
||||
"loadingDefaults": false, |
||||
"txParams": { |
||||
"from": "0x9eca64466f257793eaa52fcfff5066894b76a149", |
||||
"to": "0x0ccc8aeeaf5ce790f3b448325981a143fdef8848", |
||||
"nonce": "0xb", |
||||
"value": "0x1bc16d674ec80000", |
||||
"gas": "0x5208", |
||||
"gasPrice": "0x2540be400" |
||||
}, |
||||
"type": "standard", |
||||
"origin": "metamask", |
||||
"transactionCategory": "sentEther", |
||||
"nonceDetails": { |
||||
"params": { |
||||
"highestLocallyConfirmed": 0, |
||||
"highestSuggested": 10, |
||||
"nextNetworkNonce": 10 |
||||
}, |
||||
"local": { |
||||
"name": "local", |
||||
"nonce": 11, |
||||
"details": { |
||||
"startPoint": 10, |
||||
"highest": 11 |
||||
} |
||||
}, |
||||
"network": { |
||||
"name": "network", |
||||
"nonce": 10, |
||||
"details": { |
||||
"blockNumber": "0x62d5cc", |
||||
"baseCount": 10 |
||||
} |
||||
} |
||||
}, |
||||
"r": "0xe6828baea0a93a52779ffa5ea55e927781fb7d4be58107a29c75d314d433d055", |
||||
"s": "0x10613f984c57b8928d8ed9fce16ddda5746767e5f68f4c8fc29542e86a61f458", |
||||
"v": "0x2b", |
||||
"rawTx": "0xf86c0b8502540be400825208940ccc8aeeaf5ce790f3b448325981a143fdef8848881bc16d674ec80000802ba0e6828baea0a93a52779ffa5ea55e927781fb7d4be58107a29c75d314d433d055a010613f984c57b8928d8ed9fce16ddda5746767e5f68f4c8fc29542e86a61f458", |
||||
"hash": "0x2ccb9e2c0c64399ebc5c4ac70bebc5b537248458dee6cbce32df4b50c9e73bbd", |
||||
"submittedTime": 1589314356907, |
||||
"txReceipt": { |
||||
"blockHash": "0xfa3c8b63aaba2ef64ab328af72811dd5110a7641bd435cc6fbdfd9ea0d334542", |
||||
"blockNumber": "62d5ce", |
||||
"contractAddress": null, |
||||
"cumulativeGasUsed": "5208", |
||||
"from": "0x9eca64466f257793eaa52fcfff5066894b76a149", |
||||
"gasUsed": "5208", |
||||
"logs": [], |
||||
"logsBloom": "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", |
||||
"status": "0x1", |
||||
"to": "0x0ccc8aeeaf5ce790f3b448325981a143fdef8848", |
||||
"transactionHash": "0x2ccb9e2c0c64399ebc5c4ac70bebc5b537248458dee6cbce32df4b50c9e73bbd", |
||||
"transactionIndex": "0" |
||||
} |
||||
}, |
||||
"hasRetried": false, |
||||
"hasCancelled": false |
||||
}, |
||||
{ |
||||
"nonce": "0xa", |
||||
"initialTransaction": { |
||||
"id": 4243712234858506, |
||||
"time": 1589314345433, |
||||
"status": "confirmed", |
||||
"metamaskNetworkId": "4", |
||||
"loadingDefaults": false, |
||||
"txParams": { |
||||
"from": "0x9eca64466f257793eaa52fcfff5066894b76a149", |
||||
"to": "0xffe5bc4e8f1f969934d773fa67da095d2e491a97", |
||||
"nonce": "0xa", |
||||
"value": "0x1bc16d674ec80000", |
||||
"gas": "0x5208", |
||||
"gasPrice": "0x306dc4200" |
||||
}, |
||||
"type": "standard", |
||||
"origin": "metamask", |
||||
"transactionCategory": "sentEther", |
||||
"nonceDetails": { |
||||
"params": { |
||||
"highestLocallyConfirmed": 0, |
||||
"highestSuggested": 10, |
||||
"nextNetworkNonce": 10 |
||||
}, |
||||
"local": { |
||||
"name": "local", |
||||
"nonce": 10, |
||||
"details": { |
||||
"startPoint": 10, |
||||
"highest": 10 |
||||
} |
||||
}, |
||||
"network": { |
||||
"name": "network", |
||||
"nonce": 10, |
||||
"details": { |
||||
"blockNumber": "0x62d5cb", |
||||
"baseCount": 10 |
||||
} |
||||
} |
||||
}, |
||||
"r": "0x94b120a1df80be3dfad057b7ccac866b6b7583b63d61e5b021811c8b7ffc9a3b", |
||||
"s": "0x1778de08e29a4c8dfc3aa3e2c2338e98494ebd2c380c901d0dfba95126dde65f", |
||||
"v": "0x2c", |
||||
"rawTx": "0xf86c0a850306dc420082520894ffe5bc4e8f1f969934d773fa67da095d2e491a97881bc16d674ec80000802ca094b120a1df80be3dfad057b7ccac866b6b7583b63d61e5b021811c8b7ffc9a3ba01778de08e29a4c8dfc3aa3e2c2338e98494ebd2c380c901d0dfba95126dde65f", |
||||
"hash": "0x52604fd8d329894a747d8cf521cbbc4adb35eb9a91e3a3ba3ee32d8729c16536", |
||||
"submittedTime": 1589314348235, |
||||
"firstRetryBlockNumber": "0x62d5cc", |
||||
"txReceipt": { |
||||
"blockHash": "0x3d61a8d8a0e79e0e7a3a9206bf62f9a8e47791c527cd85cb4fcf800609234115", |
||||
"blockNumber": "62d5cd", |
||||
"contractAddress": null, |
||||
"cumulativeGasUsed": "a810", |
||||
"from": "0x9eca64466f257793eaa52fcfff5066894b76a149", |
||||
"gasUsed": "5208", |
||||
"logs": [], |
||||
"logsBloom": "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", |
||||
"status": "0x1", |
||||
"to": "0xffe5bc4e8f1f969934d773fa67da095d2e491a97", |
||||
"transactionHash": "0x52604fd8d329894a747d8cf521cbbc4adb35eb9a91e3a3ba3ee32d8729c16536", |
||||
"transactionIndex": "1" |
||||
} |
||||
}, |
||||
"primaryTransaction": { |
||||
"id": 4243712234858506, |
||||
"time": 1589314345433, |
||||
"status": "confirmed", |
||||
"metamaskNetworkId": "4", |
||||
"loadingDefaults": false, |
||||
"txParams": { |
||||
"from": "0x9eca64466f257793eaa52fcfff5066894b76a149", |
||||
"to": "0xffe5bc4e8f1f969934d773fa67da095d2e491a97", |
||||
"nonce": "0xa", |
||||
"value": "0x1bc16d674ec80000", |
||||
"gas": "0x5208", |
||||
"gasPrice": "0x306dc4200" |
||||
}, |
||||
"type": "standard", |
||||
"origin": "metamask", |
||||
"transactionCategory": "sentEther", |
||||
"nonceDetails": { |
||||
"params": { |
||||
"highestLocallyConfirmed": 0, |
||||
"highestSuggested": 10, |
||||
"nextNetworkNonce": 10 |
||||
}, |
||||
"local": { |
||||
"name": "local", |
||||
"nonce": 10, |
||||
"details": { |
||||
"startPoint": 10, |
||||
"highest": 10 |
||||
} |
||||
}, |
||||
"network": { |
||||
"name": "network", |
||||
"nonce": 10, |
||||
"details": { |
||||
"blockNumber": "0x62d5cb", |
||||
"baseCount": 10 |
||||
} |
||||
} |
||||
}, |
||||
"r": "0x94b120a1df80be3dfad057b7ccac866b6b7583b63d61e5b021811c8b7ffc9a3b", |
||||
"s": "0x1778de08e29a4c8dfc3aa3e2c2338e98494ebd2c380c901d0dfba95126dde65f", |
||||
"v": "0x2c", |
||||
"rawTx": "0xf86c0a850306dc420082520894ffe5bc4e8f1f969934d773fa67da095d2e491a97881bc16d674ec80000802ca094b120a1df80be3dfad057b7ccac866b6b7583b63d61e5b021811c8b7ffc9a3ba01778de08e29a4c8dfc3aa3e2c2338e98494ebd2c380c901d0dfba95126dde65f", |
||||
"hash": "0x52604fd8d329894a747d8cf521cbbc4adb35eb9a91e3a3ba3ee32d8729c16536", |
||||
"submittedTime": 1589314348235, |
||||
"firstRetryBlockNumber": "0x62d5cc", |
||||
"txReceipt": { |
||||
"blockHash": "0x3d61a8d8a0e79e0e7a3a9206bf62f9a8e47791c527cd85cb4fcf800609234115", |
||||
"blockNumber": "62d5cd", |
||||
"contractAddress": null, |
||||
"cumulativeGasUsed": "a810", |
||||
"from": "0x9eca64466f257793eaa52fcfff5066894b76a149", |
||||
"gasUsed": "5208", |
||||
"logs": [], |
||||
"logsBloom": "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", |
||||
"status": "0x1", |
||||
"to": "0xffe5bc4e8f1f969934d773fa67da095d2e491a97", |
||||
"transactionHash": "0x52604fd8d329894a747d8cf521cbbc4adb35eb9a91e3a3ba3ee32d8729c16536", |
||||
"transactionIndex": "1" |
||||
} |
||||
}, |
||||
"hasRetried": false, |
||||
"hasCancelled": false |
||||
}, |
||||
{ |
||||
"initialTransaction": { |
||||
"blockNumber": "6477257", |
||||
"id": 4243712234858505, |
||||
"metamaskNetworkId": "4", |
||||
"status": "confirmed", |
||||
"time": 1589314295000, |
||||
"txParams": { |
||||
"from": "0x31b98d14007bdee637298086988a0bbd31184523", |
||||
"gas": "0x5208", |
||||
"gasPrice": "0x3b9aca00", |
||||
"nonce": "0x56540", |
||||
"to": "0x9eca64466f257793eaa52fcfff5066894b76a149", |
||||
"value": "0x1043561a882930000" |
||||
}, |
||||
"hash": "0x5ca26d1cdcabef1ac2ad5b2b38604c9ced65d143efc7525f848c46f28e0e4116", |
||||
"transactionCategory": "incoming" |
||||
}, |
||||
"primaryTransaction": { |
||||
"blockNumber": "6477257", |
||||
"id": 4243712234858505, |
||||
"metamaskNetworkId": "4", |
||||
"status": "confirmed", |
||||
"time": 1589314295000, |
||||
"txParams": { |
||||
"from": "0x31b98d14007bdee637298086988a0bbd31184523", |
||||
"gas": "0x5208", |
||||
"gasPrice": "0x3b9aca00", |
||||
"nonce": "0x56540", |
||||
"to": "0x9eca64466f257793eaa52fcfff5066894b76a149", |
||||
"value": "0x1043561a882930000" |
||||
}, |
||||
"hash": "0x5ca26d1cdcabef1ac2ad5b2b38604c9ced65d143efc7525f848c46f28e0e4116", |
||||
"transactionCategory": "incoming" |
||||
}, |
||||
"hasRetried": false, |
||||
"hasCancelled": false |
||||
}, |
||||
{ |
||||
"initialTransaction": { |
||||
"blockNumber": "6454493", |
||||
"id": 4243712234858475, |
||||
"metamaskNetworkId": "4", |
||||
"status": "confirmed", |
||||
"time": 1588972833000, |
||||
"txParams": { |
||||
"from": "0x9eca64466f257793eaa52fcfff5066894b76a149", |
||||
"gas": "0x5208", |
||||
"gasPrice": "0x24e160300", |
||||
"nonce": "0x8", |
||||
"to": "0x9eca64466f257793eaa52fcfff5066894b76a149", |
||||
"value": "0x0" |
||||
}, |
||||
"hash": "0xa42b2b433e5bd2616b52e30792aedb6a3c374a752a95d43d99e2a8b143937889", |
||||
"transactionCategory": "incoming" |
||||
}, |
||||
"primaryTransaction": { |
||||
"blockNumber": "6454493", |
||||
"id": 4243712234858475, |
||||
"metamaskNetworkId": "4", |
||||
"status": "confirmed", |
||||
"time": 1588972833000, |
||||
"txParams": { |
||||
"from": "0x9eca64466f257793eaa52fcfff5066894b76a149", |
||||
"gas": "0x5208", |
||||
"gasPrice": "0x24e160300", |
||||
"nonce": "0x8", |
||||
"to": "0x9eca64466f257793eaa52fcfff5066894b76a149", |
||||
"value": "0x0" |
||||
}, |
||||
"hash": "0xa42b2b433e5bd2616b52e30792aedb6a3c374a752a95d43d99e2a8b143937889", |
||||
"transactionCategory": "incoming" |
||||
}, |
||||
"hasRetried": false, |
||||
"hasCancelled": false |
||||
}, |
||||
{ |
||||
"initialTransaction": { |
||||
"blockNumber": "6195526", |
||||
"id": 4243712234858466, |
||||
"metamaskNetworkId": "4", |
||||
"status": "confirmed", |
||||
"time": 1585087013000, |
||||
"txParams": { |
||||
"from": "0xee014609ef9e09776ac5fe00bdbfef57bcdefebb", |
||||
"gas": "0x5208", |
||||
"gasPrice": "0x77359400", |
||||
"nonce": "0x3", |
||||
"to": "0x9eca64466f257793eaa52fcfff5066894b76a149", |
||||
"value": "0xde0b6b3a7640000" |
||||
}, |
||||
"hash": "0xbcb195f393f4468945b4045cd41bcdbc2f19ad75ae92a32cf153a3004e42009a", |
||||
"transactionCategory": "incoming" |
||||
}, |
||||
"primaryTransaction": { |
||||
"blockNumber": "6195526", |
||||
"id": 4243712234858466, |
||||
"metamaskNetworkId": "4", |
||||
"status": "confirmed", |
||||
"time": 1585087013000, |
||||
"txParams": { |
||||
"from": "0xee014609ef9e09776ac5fe00bdbfef57bcdefebb", |
||||
"gas": "0x5208", |
||||
"gasPrice": "0x77359400", |
||||
"nonce": "0x3", |
||||
"to": "0x9eca64466f257793eaa52fcfff5066894b76a149", |
||||
"value": "0xde0b6b3a7640000" |
||||
}, |
||||
"hash": "0xbcb195f393f4468945b4045cd41bcdbc2f19ad75ae92a32cf153a3004e42009a", |
||||
"transactionCategory": "incoming" |
||||
}, |
||||
"hasRetried": false, |
||||
"hasCancelled": false |
||||
} |
||||
] |
@ -1 +1 @@ |
||||
export { default } from './transaction-list-item.container' |
||||
export { default } from './transaction-list-item.component' |
||||
|
@ -1,149 +1,50 @@ |
||||
.transaction-list-item { |
||||
box-sizing: border-box; |
||||
min-height: 74px; |
||||
border-bottom: 1px solid $Grey-100; |
||||
display: flex; |
||||
justify-content: center; |
||||
align-items: center; |
||||
flex-direction: column; |
||||
background: $white; |
||||
cursor: pointer; |
||||
|
||||
&__grid { |
||||
cursor: pointer; |
||||
width: 100%; |
||||
padding: 16px 20px; |
||||
display: grid; |
||||
grid-template-columns: 45px 1fr 1fr 1fr 1fr; |
||||
grid-template-areas: |
||||
"identicon action status estimated-time primary-amount" |
||||
"identicon nonce status estimated-time secondary-amount"; |
||||
grid-template-rows: 24px; |
||||
|
||||
@media screen and (max-width: $break-small) { |
||||
padding: .5rem 1rem; |
||||
grid-template-columns: 45px 5fr 3fr; |
||||
grid-template-areas: |
||||
"nonce nonce nonce nonce" |
||||
"identicon action estimated-time primary-amount" |
||||
"identicon status estimated-time secondary-amount"; |
||||
grid-template-rows: auto 24px; |
||||
} |
||||
|
||||
&:hover { |
||||
background: rgba($alto, .2); |
||||
} |
||||
&:hover { |
||||
background-color: $Grey-000; |
||||
} |
||||
|
||||
&__identicon { |
||||
grid-area: identicon; |
||||
grid-row: 1 / span 2; |
||||
align-self: center; |
||||
|
||||
@media screen and (max-width: $break-small) { |
||||
grid-row: 2 / span 2; |
||||
} |
||||
&__primary-currency { |
||||
color: $Black-100; |
||||
} |
||||
|
||||
&__action { |
||||
text-transform: capitalize; |
||||
padding: 0 0 4px 0; |
||||
white-space: nowrap; |
||||
overflow: hidden; |
||||
text-overflow: ellipsis; |
||||
grid-area: action; |
||||
color: $Grey-800; |
||||
line-height: 20px; |
||||
&__secondary-currency { |
||||
color: $Grey-500; |
||||
} |
||||
|
||||
&__status { |
||||
grid-area: status; |
||||
grid-row: 1 / span 2; |
||||
align-self: center; |
||||
|
||||
@media screen and (max-width: $break-small) { |
||||
grid-row: 3; |
||||
} |
||||
&--pending { |
||||
color: $Grey-500; |
||||
} |
||||
|
||||
&__estimated-time { |
||||
grid-area: estimated-time; |
||||
grid-row: 1 / span 2; |
||||
align-self: center; |
||||
|
||||
@media screen and (max-width: $break-small) { |
||||
grid-row: 3; |
||||
grid-column: 4; |
||||
font-size: small; |
||||
} |
||||
&--pending &__primary-currency { |
||||
color: $Grey-500; |
||||
} |
||||
|
||||
&__nonce { |
||||
font-size: .75rem; |
||||
color: #5e6064; |
||||
white-space: nowrap; |
||||
overflow: hidden; |
||||
text-overflow: ellipsis; |
||||
grid-area: nonce; |
||||
align-self: start; |
||||
|
||||
@media screen and (max-width: $break-small) { |
||||
padding-bottom: 8px; |
||||
line-height: 12px; |
||||
&__status { |
||||
&--unapproved { |
||||
color: $flamingo; |
||||
} |
||||
} |
||||
|
||||
&__amount { |
||||
white-space: nowrap; |
||||
overflow: hidden; |
||||
text-overflow: ellipsis; |
||||
min-width: 0; |
||||
max-width: 100%; |
||||
|
||||
&--primary { |
||||
text-align: end; |
||||
grid-area: primary-amount; |
||||
align-self: end; |
||||
justify-self: end; |
||||
line-height: 20px; |
||||
|
||||
@media screen and (max-width: $break-small) { |
||||
padding-bottom: 4px; |
||||
height: 100%; |
||||
color: $Grey-800; |
||||
} |
||||
&--failed { |
||||
color: $valencia; |
||||
} |
||||
|
||||
&--secondary { |
||||
text-align: end; |
||||
font-size: .75rem; |
||||
grid-area: secondary-amount; |
||||
align-self: start; |
||||
justify-self: end; |
||||
color: $Grey-500; |
||||
&--cancelled { |
||||
color: $valencia; |
||||
} |
||||
} |
||||
|
||||
&__retry { |
||||
background: #d1edff; |
||||
border-radius: 12px; |
||||
font-size: .75rem; |
||||
padding: 4px 12px; |
||||
cursor: pointer; |
||||
margin-top: 8px; |
||||
|
||||
@media screen and (max-width: $break-small) { |
||||
font-size: .5rem; |
||||
&--queued { |
||||
color: $Grey-500; |
||||
} |
||||
} |
||||
|
||||
&__expander { |
||||
max-height: 0px; |
||||
width: 100%; |
||||
overflow: hidden; |
||||
|
||||
&--show { |
||||
max-height: 1000px; |
||||
transition: max-height 700ms ease-out; |
||||
&__pending-actions { |
||||
padding-top: 12px; |
||||
display: flex; |
||||
.button { |
||||
font-size: 0.625rem; |
||||
padding: 8px; |
||||
width: 75px; |
||||
white-space: nowrap; |
||||
line-height: 1rem; |
||||
} |
||||
} |
||||
} |
||||
|
@ -1,278 +1,200 @@ |
||||
import React, { PureComponent } from 'react' |
||||
import React, { useMemo, useState, useCallback } from 'react' |
||||
import PropTypes from 'prop-types' |
||||
import classnames from 'classnames' |
||||
import Identicon from '../../ui/identicon' |
||||
import TransactionStatus from '../transaction-status' |
||||
import TransactionAction from '../transaction-action' |
||||
import UserPreferencedCurrencyDisplay from '../user-preferenced-currency-display' |
||||
import TokenCurrencyDisplay from '../../ui/token-currency-display' |
||||
import TransactionListItemDetails from '../transaction-list-item-details' |
||||
import TransactionTimeRemaining from '../transaction-time-remaining' |
||||
import ListItem from '../../ui/list-item' |
||||
import { useTransactionDisplayData } from '../../../hooks/useTransactionDisplayData' |
||||
import Approve from '../../ui/icon/approve-icon.component' |
||||
import Interaction from '../../ui/icon/interaction-icon.component' |
||||
import Receive from '../../ui/icon/receive-icon.component' |
||||
import Preloader from '../../ui/icon/preloader' |
||||
import Send from '../../ui/icon/send-icon.component' |
||||
import { useI18nContext } from '../../../hooks/useI18nContext' |
||||
import { useCancelTransaction } from '../../../hooks/useCancelTransaction' |
||||
import { useRetryTransaction } from '../../../hooks/useRetryTransaction' |
||||
import Button from '../../ui/button' |
||||
import Tooltip from '../../ui/tooltip' |
||||
import TransactionListItemDetails from '../transaction-list-item-details/transaction-list-item-details.component' |
||||
import { useHistory } from 'react-router-dom' |
||||
import { CONFIRM_TRANSACTION_ROUTE } from '../../../helpers/constants/routes' |
||||
import { UNAPPROVED_STATUS, TOKEN_METHOD_TRANSFER } from '../../../helpers/constants/transactions' |
||||
import { PRIMARY, SECONDARY } from '../../../helpers/constants/common' |
||||
import { getStatusKey } from '../../../helpers/utils/transactions.util' |
||||
import { ENVIRONMENT_TYPE_FULLSCREEN } from '../../../../../app/scripts/lib/enums' |
||||
import { getEnvironmentType } from '../../../../../app/scripts/lib/util' |
||||
|
||||
export default class TransactionListItem extends PureComponent { |
||||
static propTypes = { |
||||
assetImages: PropTypes.object, |
||||
history: PropTypes.object, |
||||
methodData: PropTypes.object, |
||||
nonceAndDate: PropTypes.string, |
||||
primaryTransaction: PropTypes.object, |
||||
retryTransaction: PropTypes.func, |
||||
setSelectedToken: PropTypes.func, |
||||
showCancelModal: PropTypes.func, |
||||
showCancel: PropTypes.bool, |
||||
hasEnoughCancelGas: PropTypes.bool, |
||||
showSpeedUp: PropTypes.bool, |
||||
isEarliestNonce: PropTypes.bool, |
||||
showFiat: PropTypes.bool, |
||||
token: PropTypes.object, |
||||
tokenData: PropTypes.object, |
||||
transaction: PropTypes.object, |
||||
transactionGroup: PropTypes.object, |
||||
value: PropTypes.string, |
||||
fetchBasicGasAndTimeEstimates: PropTypes.func, |
||||
fetchGasEstimates: PropTypes.func, |
||||
rpcPrefs: PropTypes.object, |
||||
data: PropTypes.string, |
||||
getContractMethodData: PropTypes.func, |
||||
isDeposit: PropTypes.bool, |
||||
transactionTimeFeatureActive: PropTypes.bool, |
||||
firstPendingTransactionId: PropTypes.number, |
||||
} |
||||
|
||||
static defaultProps = { |
||||
showFiat: true, |
||||
} |
||||
|
||||
static contextTypes = { |
||||
metricsEvent: PropTypes.func, |
||||
} |
||||
|
||||
state = { |
||||
showTransactionDetails: false, |
||||
import Identicon from '../../ui/identicon/identicon.component' |
||||
import { |
||||
TRANSACTION_CATEGORY_APPROVAL, |
||||
TRANSACTION_CATEGORY_SIGNATURE_REQUEST, |
||||
TRANSACTION_CATEGORY_INTERACTION, |
||||
TRANSACTION_CATEGORY_SEND, |
||||
TRANSACTION_CATEGORY_RECEIVE, |
||||
UNAPPROVED_STATUS, |
||||
FAILED_STATUS, |
||||
CANCELLED_STATUS, |
||||
} from '../../../helpers/constants/transactions' |
||||
import { useShouldShowSpeedUp } from '../../../hooks/useShouldShowSpeedUp' |
||||
|
||||
|
||||
export default function TransactionListItem ({ transactionGroup, isEarliestNonce = false }) { |
||||
const t = useI18nContext() |
||||
const history = useHistory() |
||||
const { hasCancelled } = transactionGroup |
||||
const [showDetails, setShowDetails] = useState(false) |
||||
|
||||
const { initialTransaction: { id } } = transactionGroup |
||||
|
||||
const [cancelEnabled, cancelTransaction] = useCancelTransaction(transactionGroup) |
||||
const retryTransaction = useRetryTransaction(transactionGroup) |
||||
const shouldShowSpeedUp = useShouldShowSpeedUp(transactionGroup, isEarliestNonce) |
||||
|
||||
const { |
||||
title, |
||||
subtitle, |
||||
date, |
||||
category, |
||||
primaryCurrency, |
||||
recipientAddress, |
||||
secondaryCurrency, |
||||
status, |
||||
isPending, |
||||
senderAddress, |
||||
} = useTransactionDisplayData(transactionGroup) |
||||
|
||||
const isApprove = category === TRANSACTION_CATEGORY_APPROVAL |
||||
const isSignatureReq = category === TRANSACTION_CATEGORY_SIGNATURE_REQUEST |
||||
const isInteraction = category === TRANSACTION_CATEGORY_INTERACTION |
||||
const isSend = category === TRANSACTION_CATEGORY_SEND |
||||
const isReceive = category === TRANSACTION_CATEGORY_RECEIVE |
||||
const isUnapproved = status === UNAPPROVED_STATUS |
||||
const isFailed = status === FAILED_STATUS |
||||
const isCancelled = status === CANCELLED_STATUS |
||||
|
||||
const color = isFailed ? '#D73A49' : '#2F80ED' |
||||
|
||||
let Icon |
||||
if (isApprove) { |
||||
Icon = Approve |
||||
} else if (isSend) { |
||||
Icon = Send |
||||
} else if (isReceive) { |
||||
Icon = Receive |
||||
} else if (isInteraction) { |
||||
Icon = Interaction |
||||
} |
||||
|
||||
componentDidMount () { |
||||
if (this.props.data) { |
||||
this.props.getContractMethodData(this.props.data) |
||||
} |
||||
|
||||
let subtitleStatus = <span><span className="transaction-list-item__date">{date}</span> · </span> |
||||
if (isUnapproved) { |
||||
subtitleStatus = ( |
||||
<span><span className="transaction-list-item__status--unapproved">{t('unapproved')}</span> · </span> |
||||
) |
||||
} else if (isFailed) { |
||||
subtitleStatus = ( |
||||
<span><span className="transaction-list-item__status--failed">{t('failed')}</span> · </span> |
||||
) |
||||
} else if (isCancelled) { |
||||
subtitleStatus = ( |
||||
<span><span className="transaction-list-item__status--cancelled">{t('cancelled')}</span> · </span> |
||||
) |
||||
} else if (isPending && !isEarliestNonce) { |
||||
subtitleStatus = ( |
||||
<span><span className="transaction-list-item__status--queued">{t('queued')}</span> · </span> |
||||
) |
||||
} |
||||
|
||||
handleClick = () => { |
||||
const { |
||||
transaction, |
||||
history, |
||||
} = this.props |
||||
const { id, status } = transaction |
||||
const { showTransactionDetails } = this.state |
||||
const className = classnames('transaction-list-item', { 'transaction-list-item--pending': isPending }) |
||||
|
||||
if (status === UNAPPROVED_STATUS) { |
||||
const toggleShowDetails = useCallback(() => { |
||||
if (isUnapproved) { |
||||
history.push(`${CONFIRM_TRANSACTION_ROUTE}/${id}`) |
||||
return |
||||
} |
||||
|
||||
if (!showTransactionDetails) { |
||||
this.context.metricsEvent({ |
||||
eventOpts: { |
||||
category: 'Navigation', |
||||
action: 'Home', |
||||
name: 'Expand Transaction', |
||||
}, |
||||
}) |
||||
setShowDetails((prev) => !prev) |
||||
}, [isUnapproved, id]) |
||||
|
||||
const cancelButton = useMemo(() => { |
||||
const cancelButton = ( |
||||
<Button |
||||
onClick={cancelTransaction} |
||||
rounded |
||||
className="transaction-list-item__header-button" |
||||
disabled={!cancelEnabled} |
||||
> |
||||
{ t('cancel') } |
||||
</Button> |
||||
) |
||||
if (hasCancelled || !isPending || isUnapproved) { |
||||
return null |
||||
} |
||||
|
||||
this.setState({ showTransactionDetails: !showTransactionDetails }) |
||||
} |
||||
|
||||
handleCancel = (id) => { |
||||
const { |
||||
primaryTransaction: { txParams: { gasPrice } } = {}, |
||||
transaction: { id: initialTransactionId }, |
||||
showCancelModal, |
||||
} = this.props |
||||
|
||||
const cancelId = id || initialTransactionId |
||||
showCancelModal(cancelId, gasPrice) |
||||
} |
||||
return !cancelEnabled ? ( |
||||
<Tooltip title={t('notEnoughGas')}> |
||||
<div> |
||||
{cancelButton} |
||||
</div> |
||||
</Tooltip> |
||||
) : cancelButton |
||||
|
||||
/** |
||||
* @name handleRetry |
||||
* @description Resubmits a transaction. Retrying a transaction within a list of transactions with |
||||
* the same nonce requires keeping the original value while increasing the gas price of the latest |
||||
* transaction. |
||||
* @param {number} id - Transaction id |
||||
*/ |
||||
handleRetry = (id) => { |
||||
const { |
||||
primaryTransaction: { txParams: { gasPrice } } = {}, |
||||
transaction: { txParams: { to } = {}, id: initialTransactionId }, |
||||
methodData: { name } = {}, |
||||
setSelectedToken, |
||||
retryTransaction, |
||||
fetchBasicGasAndTimeEstimates, |
||||
fetchGasEstimates, |
||||
} = this.props |
||||
}, [cancelEnabled, cancelTransaction, hasCancelled]) |
||||
|
||||
if (name === TOKEN_METHOD_TRANSFER) { |
||||
setSelectedToken(to) |
||||
const speedUpButton = useMemo(() => { |
||||
if (!shouldShowSpeedUp || !isPending || isUnapproved) { |
||||
return null |
||||
} |
||||
|
||||
const retryId = id || initialTransactionId |
||||
|
||||
this.context.metricsEvent({ |
||||
eventOpts: { |
||||
category: 'Navigation', |
||||
action: 'Activity Log', |
||||
name: 'Clicked "Speed Up"', |
||||
}, |
||||
}) |
||||
|
||||
return fetchBasicGasAndTimeEstimates() |
||||
.then((basicEstimates) => fetchGasEstimates(basicEstimates.blockTime)) |
||||
.then(retryTransaction(retryId, gasPrice)) |
||||
} |
||||
|
||||
renderPrimaryCurrency () { |
||||
const { token, primaryTransaction: { txParams: { data } = {} } = {}, value, isDeposit } = this.props |
||||
|
||||
return token |
||||
? ( |
||||
<TokenCurrencyDisplay |
||||
className="transaction-list-item__amount transaction-list-item__amount--primary" |
||||
token={token} |
||||
transactionData={data} |
||||
prefix="-" |
||||
/> |
||||
) : ( |
||||
<UserPreferencedCurrencyDisplay |
||||
className="transaction-list-item__amount transaction-list-item__amount--primary" |
||||
value={value} |
||||
type={PRIMARY} |
||||
prefix={isDeposit ? '' : '-'} |
||||
/> |
||||
) |
||||
} |
||||
|
||||
renderSecondaryCurrency () { |
||||
const { token, value, showFiat } = this.props |
||||
|
||||
return token || !showFiat |
||||
? null |
||||
: ( |
||||
<UserPreferencedCurrencyDisplay |
||||
className="transaction-list-item__amount transaction-list-item__amount--secondary" |
||||
value={value} |
||||
prefix="-" |
||||
type={SECONDARY} |
||||
/> |
||||
) |
||||
} |
||||
|
||||
render () { |
||||
const { |
||||
assetImages, |
||||
transaction, |
||||
methodData, |
||||
nonceAndDate, |
||||
primaryTransaction, |
||||
showCancel, |
||||
hasEnoughCancelGas, |
||||
showSpeedUp, |
||||
tokenData, |
||||
transactionGroup, |
||||
rpcPrefs, |
||||
isEarliestNonce, |
||||
firstPendingTransactionId, |
||||
transactionTimeFeatureActive, |
||||
} = this.props |
||||
const { txParams = {} } = transaction |
||||
const { showTransactionDetails } = this.state |
||||
const fromAddress = txParams.from |
||||
const toAddress = tokenData |
||||
? (tokenData.params && tokenData.params[0] && tokenData.params[0].value) || txParams.to |
||||
: txParams.to |
||||
|
||||
const isFullScreen = getEnvironmentType() === ENVIRONMENT_TYPE_FULLSCREEN |
||||
const showEstimatedTime = transactionTimeFeatureActive && |
||||
(transaction.id === firstPendingTransactionId) && |
||||
isFullScreen |
||||
|
||||
return ( |
||||
<div className="transaction-list-item"> |
||||
<div |
||||
className="transaction-list-item__grid" |
||||
onClick={this.handleClick} |
||||
> |
||||
<Identicon |
||||
className="transaction-list-item__identicon" |
||||
address={toAddress} |
||||
diameter={36} |
||||
image={assetImages[toAddress]} |
||||
/> |
||||
<TransactionAction |
||||
transaction={transaction} |
||||
methodData={methodData} |
||||
className="transaction-list-item__action" |
||||
/> |
||||
<div |
||||
className="transaction-list-item__nonce" |
||||
title={nonceAndDate} |
||||
> |
||||
{ nonceAndDate } |
||||
</div> |
||||
<TransactionStatus |
||||
className="transaction-list-item__status" |
||||
statusKey={getStatusKey(primaryTransaction)} |
||||
title={( |
||||
(primaryTransaction.err && primaryTransaction.err.rpc) |
||||
? primaryTransaction.err.rpc.message |
||||
: primaryTransaction.err && primaryTransaction.err.message |
||||
)} |
||||
<Button |
||||
type="secondary" |
||||
rounded |
||||
onClick={retryTransaction} |
||||
className="transaction-list-item-details__header-button" |
||||
> |
||||
{ t('speedUp') } |
||||
</Button> |
||||
) |
||||
}, [shouldShowSpeedUp, isPending, retryTransaction]) |
||||
|
||||
return ( |
||||
<> |
||||
<ListItem |
||||
onClick={toggleShowDetails} |
||||
className={className} |
||||
title={title} |
||||
titleIcon={!isUnapproved && isPending && isEarliestNonce && ( |
||||
<Preloader |
||||
size={16} |
||||
color="#D73A49" |
||||
/> |
||||
{ showEstimatedTime |
||||
? ( |
||||
<TransactionTimeRemaining |
||||
className="transaction-list-item__estimated-time" |
||||
transaction={ primaryTransaction } |
||||
/> |
||||
) |
||||
: null |
||||
} |
||||
{ this.renderPrimaryCurrency() } |
||||
{ this.renderSecondaryCurrency() } |
||||
</div> |
||||
<div |
||||
className={classnames('transaction-list-item__expander', { |
||||
'transaction-list-item__expander--show': showTransactionDetails, |
||||
})} |
||||
> |
||||
{ |
||||
showTransactionDetails && ( |
||||
<div className="transaction-list-item__details-container"> |
||||
<TransactionListItemDetails |
||||
transactionGroup={transactionGroup} |
||||
onRetry={this.handleRetry} |
||||
showSpeedUp={showSpeedUp} |
||||
showRetry={getStatusKey(primaryTransaction) === 'failed'} |
||||
isEarliestNonce={isEarliestNonce} |
||||
onCancel={this.handleCancel} |
||||
showCancel={showCancel} |
||||
cancelDisabled={!hasEnoughCancelGas} |
||||
rpcPrefs={rpcPrefs} |
||||
senderAddress={fromAddress} |
||||
recipientAddress={toAddress} |
||||
/> |
||||
</div> |
||||
) |
||||
} |
||||
)} |
||||
icon={isSignatureReq ? <Identicon diameter={25} /> : <Icon color={color} size={28} />} |
||||
subtitle={subtitle} |
||||
subtitleStatus={subtitleStatus} |
||||
rightContent={!isSignatureReq && ( |
||||
<> |
||||
<h2 className="transaction-list-item__primary-currency">{primaryCurrency}</h2> |
||||
<h3 className="transaction-list-item__secondary-currency">{secondaryCurrency}</h3> |
||||
</> |
||||
)} |
||||
> |
||||
<div className="transaction-list-item__pending-actions"> |
||||
{speedUpButton} |
||||
{cancelButton} |
||||
</div> |
||||
</div> |
||||
) |
||||
} |
||||
</ListItem> |
||||
{showDetails && ( |
||||
<TransactionListItemDetails |
||||
title={title} |
||||
onClose={toggleShowDetails} |
||||
transactionGroup={transactionGroup} |
||||
senderAddress={senderAddress} |
||||
recipientAddress={recipientAddress} |
||||
onRetry={retryTransaction} |
||||
showRetry={isFailed} |
||||
showSpeedUp={shouldShowSpeedUp} |
||||
isEarliestNonce={isEarliestNonce} |
||||
onCancel={cancelTransaction} |
||||
showCancel={isPending && !hasCancelled} |
||||
cancelDisabled={!cancelEnabled} |
||||
/> |
||||
)} |
||||
</> |
||||
) |
||||
} |
||||
|
||||
TransactionListItem.propTypes = { |
||||
transactionGroup: PropTypes.object.isRequired, |
||||
isEarliestNonce: PropTypes.bool, |
||||
} |
||||
|
@ -1,113 +0,0 @@ |
||||
import { connect } from 'react-redux' |
||||
import { withRouter } from 'react-router-dom' |
||||
import { compose } from 'redux' |
||||
import TransactionListItem from './transaction-list-item.component' |
||||
import { setSelectedToken, showModal, showSidebar, getContractMethodData } from '../../../store/actions' |
||||
import { hexToDecimal } from '../../../helpers/utils/conversions.util' |
||||
import { getTokenData } from '../../../helpers/utils/transactions.util' |
||||
import { getHexGasTotal, increaseLastGasPrice } from '../../../helpers/utils/confirm-tx.util' |
||||
import { formatDate } from '../../../helpers/utils/util' |
||||
import { |
||||
fetchGasEstimates, |
||||
fetchBasicGasAndTimeEstimates, |
||||
setCustomGasPriceForRetry, |
||||
setCustomGasLimit, |
||||
} from '../../../ducks/gas/gas.duck' |
||||
import { |
||||
getIsMainnet, |
||||
getPreferences, |
||||
getSelectedAddress, |
||||
conversionRateSelector, |
||||
getKnownMethodData, |
||||
getFeatureFlags, |
||||
} from '../../../selectors' |
||||
import { isBalanceSufficient } from '../../../pages/send/send.utils' |
||||
|
||||
const mapStateToProps = (state, ownProps) => { |
||||
const { metamask: { accounts, provider, frequentRpcListDetail } } = state |
||||
const { showFiatInTestnets } = getPreferences(state) |
||||
const isMainnet = getIsMainnet(state) |
||||
const { transactionGroup: { primaryTransaction } = {} } = ownProps |
||||
const { txParams: { gas: gasLimit, gasPrice, data } = {}, transactionCategory } = primaryTransaction |
||||
const selectedAddress = getSelectedAddress(state) |
||||
const selectedAccountBalance = accounts[selectedAddress].balance |
||||
const isDeposit = transactionCategory === 'incoming' |
||||
const selectRpcInfo = frequentRpcListDetail.find((rpcInfo) => rpcInfo.rpcUrl === provider.rpcTarget) |
||||
const { rpcPrefs } = selectRpcInfo || {} |
||||
|
||||
const hasEnoughCancelGas = primaryTransaction.txParams && isBalanceSufficient({ |
||||
amount: '0x0', |
||||
gasTotal: getHexGasTotal({ |
||||
gasPrice: increaseLastGasPrice(gasPrice), |
||||
gasLimit, |
||||
}), |
||||
balance: selectedAccountBalance, |
||||
conversionRate: conversionRateSelector(state), |
||||
}) |
||||
|
||||
const transactionTimeFeatureActive = getFeatureFlags(state).transactionTime |
||||
|
||||
return { |
||||
methodData: getKnownMethodData(state, data) || {}, |
||||
showFiat: (isMainnet || !!showFiatInTestnets), |
||||
selectedAccountBalance, |
||||
hasEnoughCancelGas, |
||||
rpcPrefs, |
||||
isDeposit, |
||||
transactionTimeFeatureActive, |
||||
} |
||||
} |
||||
|
||||
const mapDispatchToProps = (dispatch) => { |
||||
return { |
||||
fetchBasicGasAndTimeEstimates: () => dispatch(fetchBasicGasAndTimeEstimates()), |
||||
fetchGasEstimates: (blockTime) => dispatch(fetchGasEstimates(blockTime)), |
||||
setSelectedToken: (tokenAddress) => dispatch(setSelectedToken(tokenAddress)), |
||||
getContractMethodData: (methodData) => dispatch(getContractMethodData(methodData)), |
||||
retryTransaction: (transaction, gasPrice) => { |
||||
dispatch(setCustomGasPriceForRetry(gasPrice || transaction.txParams.gasPrice)) |
||||
dispatch(setCustomGasLimit(transaction.txParams.gas)) |
||||
dispatch(showSidebar({ |
||||
transitionName: 'sidebar-left', |
||||
type: 'customize-gas', |
||||
props: { transaction }, |
||||
})) |
||||
}, |
||||
showCancelModal: (transactionId, originalGasPrice) => { |
||||
return dispatch(showModal({ name: 'CANCEL_TRANSACTION', transactionId, originalGasPrice })) |
||||
}, |
||||
} |
||||
} |
||||
|
||||
const mergeProps = (stateProps, dispatchProps, ownProps) => { |
||||
const { transactionGroup: { primaryTransaction, initialTransaction } = {} } = ownProps |
||||
const { isDeposit } = stateProps |
||||
const { retryTransaction, ...restDispatchProps } = dispatchProps |
||||
const { txParams: { nonce, data } = {}, time = 0 } = initialTransaction |
||||
const { txParams: { value } = {} } = primaryTransaction |
||||
|
||||
const tokenData = data && getTokenData(data) |
||||
const nonceAndDate = nonce && !isDeposit ? `#${hexToDecimal(nonce)} - ${formatDate(time)}` : formatDate(time) |
||||
|
||||
return { |
||||
...stateProps, |
||||
...restDispatchProps, |
||||
...ownProps, |
||||
value, |
||||
nonceAndDate, |
||||
tokenData, |
||||
transaction: initialTransaction, |
||||
primaryTransaction, |
||||
retryTransaction: (transactionId, gasPrice) => { |
||||
const { transactionGroup: { transactions = [] } } = ownProps |
||||
const transaction = transactions.find((tx) => tx.id === transactionId) || {} |
||||
const increasedGasPrice = increaseLastGasPrice(gasPrice) |
||||
retryTransaction(transaction, increasedGasPrice) |
||||
}, |
||||
} |
||||
} |
||||
|
||||
export default compose( |
||||
withRouter, |
||||
connect(mapStateToProps, mapDispatchToProps, mergeProps), |
||||
)(TransactionListItem) |
@ -1 +1 @@ |
||||
export { default } from './transaction-list.container' |
||||
export { default } from './transaction-list.component' |
||||
|
@ -1,45 +0,0 @@ |
||||
import { connect } from 'react-redux' |
||||
import PropTypes from 'prop-types' |
||||
import TransactionList from './transaction-list.component' |
||||
import { |
||||
getAssetImages, |
||||
getFeatureFlags, |
||||
getSelectedAddress, |
||||
selectedTokenSelector, |
||||
nonceSortedCompletedTransactionsSelector, |
||||
nonceSortedPendingTransactionsSelector, |
||||
} from '../../../selectors' |
||||
import { fetchBasicGasAndTimeEstimates, fetchGasEstimates } from '../../../ducks/gas/gas.duck' |
||||
|
||||
const mapStateToProps = (state) => { |
||||
const pendingTransactions = nonceSortedPendingTransactionsSelector(state) |
||||
const firstPendingTransactionId = pendingTransactions[0] && pendingTransactions[0].primaryTransaction.id |
||||
return { |
||||
completedTransactions: nonceSortedCompletedTransactionsSelector(state), |
||||
pendingTransactions, |
||||
firstPendingTransactionId, |
||||
selectedToken: selectedTokenSelector(state), |
||||
selectedAddress: getSelectedAddress(state), |
||||
assetImages: getAssetImages(state), |
||||
transactionTimeFeatureActive: getFeatureFlags(state).transactionTime, |
||||
} |
||||
} |
||||
|
||||
const mapDispatchToProps = (dispatch) => { |
||||
return { |
||||
fetchGasEstimates: (blockTime) => dispatch(fetchGasEstimates(blockTime)), |
||||
fetchBasicGasAndTimeEstimates: () => dispatch(fetchBasicGasAndTimeEstimates()), |
||||
} |
||||
} |
||||
|
||||
const TransactionListContainer = connect(mapStateToProps, mapDispatchToProps)(TransactionList) |
||||
|
||||
TransactionListContainer.propTypes = { |
||||
isWideViewport: PropTypes.bool, |
||||
} |
||||
|
||||
TransactionListContainer.defaultProps = { |
||||
isWideViewport: false, |
||||
} |
||||
|
||||
export default TransactionListContainer |
@ -0,0 +1,103 @@ |
||||
import * as reactRedux from 'react-redux' |
||||
import assert from 'assert' |
||||
import { renderHook } from '@testing-library/react-hooks' |
||||
import sinon from 'sinon' |
||||
import transactions from '../../../../test/data/transaction-data.json' |
||||
import { getConversionRate, getSelectedAccount } from '../../selectors' |
||||
import { useCancelTransaction } from '../useCancelTransaction' |
||||
import { showModal } from '../../store/actions' |
||||
import { increaseLastGasPrice } from '../../helpers/utils/confirm-tx.util' |
||||
|
||||
|
||||
describe('useCancelTransaction', function () { |
||||
let useSelector |
||||
const dispatch = sinon.spy() |
||||
|
||||
before(function () { |
||||
sinon.stub(reactRedux, 'useDispatch').returns(dispatch) |
||||
}) |
||||
|
||||
afterEach(function () { |
||||
dispatch.resetHistory() |
||||
}) |
||||
|
||||
describe('when account has insufficent balance to cover gas', function () { |
||||
before(function () { |
||||
useSelector = sinon.stub(reactRedux, 'useSelector') |
||||
useSelector.callsFake((selector) => { |
||||
if (selector === getConversionRate) { |
||||
return 280.46 |
||||
} else if (selector === getSelectedAccount) { |
||||
return { |
||||
balance: '0x3', |
||||
} |
||||
} |
||||
}) |
||||
}) |
||||
transactions.forEach((transactionGroup) => { |
||||
const originalGasPrice = transactionGroup.primaryTransaction.txParams?.gasPrice |
||||
const gasPrice = originalGasPrice && increaseLastGasPrice(originalGasPrice) |
||||
it(`should indicate account has insufficient funds to cover ${gasPrice} gas price`, function () { |
||||
const { result } = renderHook(() => useCancelTransaction(transactionGroup)) |
||||
assert.equal(result.current[0], false) |
||||
}) |
||||
it(`should return a function that is a noop`, function () { |
||||
const { result } = renderHook(() => useCancelTransaction(transactionGroup)) |
||||
assert.equal(typeof result.current[1], 'function') |
||||
result.current[1]({ preventDefault: () => {}, stopPropagation: () => {} }) |
||||
assert.equal(dispatch.notCalled, true) |
||||
}) |
||||
}) |
||||
after(function () { |
||||
useSelector.restore() |
||||
}) |
||||
}) |
||||
|
||||
|
||||
describe('when account has sufficient balance to cover gas', function () { |
||||
before(function () { |
||||
useSelector = sinon.stub(reactRedux, 'useSelector') |
||||
useSelector.callsFake((selector) => { |
||||
if (selector === getConversionRate) { |
||||
return 280.46 |
||||
} else if (selector === getSelectedAccount) { |
||||
return { |
||||
balance: '0x9C2007651B2500000', |
||||
} |
||||
} |
||||
}) |
||||
}) |
||||
transactions.forEach((transactionGroup) => { |
||||
const originalGasPrice = transactionGroup.primaryTransaction.txParams?.gasPrice |
||||
const gasPrice = originalGasPrice && increaseLastGasPrice(originalGasPrice) |
||||
const transactionId = transactionGroup.initialTransaction.id |
||||
it(`should indicate account has funds to cover ${gasPrice} gas price`, function () { |
||||
const { result } = renderHook(() => useCancelTransaction(transactionGroup)) |
||||
assert.equal(result.current[0], true) |
||||
}) |
||||
it(`should return a function that kicks off cancellation for id ${transactionId}`, function () { |
||||
const { result } = renderHook(() => useCancelTransaction(transactionGroup)) |
||||
assert.equal(typeof result.current[1], 'function') |
||||
result.current[1]({ preventDefault: () => {}, stopPropagation: () => {} }) |
||||
assert.equal( |
||||
dispatch.calledWith( |
||||
showModal({ |
||||
name: 'CANCEL_TRANSACTION', |
||||
transactionId, |
||||
originalGasPrice, |
||||
}) |
||||
), |
||||
true |
||||
) |
||||
}) |
||||
}) |
||||
after(function () { |
||||
useSelector.restore() |
||||
}) |
||||
}) |
||||
|
||||
|
||||
after(function () { |
||||
sinon.restore() |
||||
}) |
||||
}) |
@ -0,0 +1,66 @@ |
||||
import * as reactRedux from 'react-redux' |
||||
import assert from 'assert' |
||||
import { renderHook } from '@testing-library/react-hooks' |
||||
import sinon from 'sinon' |
||||
import transactions from '../../../../test/data/transaction-data.json' |
||||
import * as methodDataHook from '../useMethodData' |
||||
import * as metricEventHook from '../useMetricEvent' |
||||
import { showSidebar } from '../../store/actions' |
||||
import { useRetryTransaction } from '../useRetryTransaction' |
||||
|
||||
describe('useRetryTransaction', function () { |
||||
describe('when transaction meets retry enabled criteria', function () { |
||||
const dispatch = sinon.spy(() => Promise.resolve({ blockTime: 0 })) |
||||
const trackEvent = sinon.spy() |
||||
const event = { preventDefault: () => {}, stopPropagation: () => {} } |
||||
|
||||
before(function () { |
||||
sinon.stub(reactRedux, 'useDispatch').returns(dispatch) |
||||
sinon.stub(methodDataHook, 'useMethodData').returns({}) |
||||
sinon.stub(metricEventHook, 'useMetricEvent').returns(trackEvent) |
||||
}) |
||||
|
||||
afterEach(function () { |
||||
dispatch.resetHistory() |
||||
trackEvent.resetHistory() |
||||
}) |
||||
const retryEnabledTransaction = { |
||||
...transactions[0], |
||||
transactions: [ |
||||
{ |
||||
submittedTime: new Date() - 5001, |
||||
}, |
||||
], |
||||
hasRetried: false, |
||||
} |
||||
|
||||
it('retryTransaction function should track metrics', function () { |
||||
const { result } = renderHook(() => useRetryTransaction(retryEnabledTransaction, true)) |
||||
const retry = result.current |
||||
retry(event) |
||||
assert.equal(trackEvent.calledOnce, true) |
||||
}) |
||||
|
||||
it('retryTransaction function should show retry sidebar', async function () { |
||||
const { result } = renderHook(() => useRetryTransaction(retryEnabledTransaction, true)) |
||||
const retry = result.current |
||||
await retry(event) |
||||
const calls = dispatch.getCalls() |
||||
assert.equal(calls.length, 5) |
||||
assert.equal( |
||||
dispatch.calledWith( |
||||
showSidebar({ |
||||
transitionName: 'sidebar-left', |
||||
type: 'customize-gas', |
||||
props: { transaction: retryEnabledTransaction.initialTransaction }, |
||||
}) |
||||
), |
||||
true |
||||
) |
||||
}) |
||||
|
||||
after(function () { |
||||
sinon.restore() |
||||
}) |
||||
}) |
||||
}) |
@ -0,0 +1,76 @@ |
||||
import { useTokenData } from '../useTokenData' |
||||
import assert from 'assert' |
||||
import { renderHook } from '@testing-library/react-hooks' |
||||
|
||||
const tests = [ |
||||
{ |
||||
data: '0xa9059cbb000000000000000000000000ffe5bc4e8f1f969934d773fa67da095d2e491a970000000000000000000000000000000000000000000000000000000000003a98', |
||||
tokenData: { |
||||
'name': 'transfer', |
||||
'params': [ |
||||
{ |
||||
'name': '_to', |
||||
'value': '0xffe5bc4e8f1f969934d773fa67da095d2e491a97', |
||||
'type': 'address', |
||||
}, |
||||
{ |
||||
'name': '_value', |
||||
'value': '15000', |
||||
'type': 'uint256', |
||||
}, |
||||
], |
||||
}, |
||||
}, |
||||
{ |
||||
data: '0xa9059cbb000000000000000000000000ffe5bc4e8f1f969934d773fa67da095d2e491a9700000000000000000000000000000000000000000000000000000000000061a8', |
||||
tokenData: { |
||||
'name': 'transfer', |
||||
'params': [ |
||||
{ |
||||
'name': '_to', |
||||
'value': '0xffe5bc4e8f1f969934d773fa67da095d2e491a97', |
||||
'type': 'address', |
||||
}, |
||||
{ |
||||
'name': '_value', |
||||
'value': '25000', |
||||
'type': 'uint256', |
||||
}, |
||||
], |
||||
}, |
||||
}, |
||||
{ |
||||
data: '0xa9059cbb000000000000000000000000ffe5bc4e8f1f969934d773fa67da095d2e491a970000000000000000000000000000000000000000000000000000000000002710', |
||||
tokenData: { |
||||
'name': 'transfer', |
||||
'params': [ |
||||
{ |
||||
'name': '_to', |
||||
'value': '0xffe5bc4e8f1f969934d773fa67da095d2e491a97', |
||||
'type': 'address', |
||||
}, |
||||
{ |
||||
'name': '_value', |
||||
'value': '10000', |
||||
'type': 'uint256', |
||||
}, |
||||
], |
||||
}, |
||||
}, |
||||
{ |
||||
data: undefined, |
||||
tokenData: null, |
||||
}, |
||||
] |
||||
|
||||
describe('useTokenData', function () { |
||||
tests.forEach((test) => { |
||||
const testTitle = test.tokenData !== null |
||||
? `should return properly decoded data with _value ${test.tokenData.params[1].value}` |
||||
: `should return null when no data provided` |
||||
it(testTitle, function () { |
||||
const { result } = renderHook(() => useTokenData(test.data)) |
||||
assert.deepEqual(result.current, test.tokenData) |
||||
}) |
||||
}) |
||||
}) |
@ -0,0 +1,146 @@ |
||||
import * as reactRedux from 'react-redux' |
||||
import assert from 'assert' |
||||
import { renderHook } from '@testing-library/react-hooks' |
||||
import sinon from 'sinon' |
||||
import transactions from '../../../../test/data/transaction-data.json' |
||||
import { useTransactionDisplayData } from '../useTransactionDisplayData' |
||||
import { tokenSelector, getPreferences, getShouldShowFiat, getNativeCurrency, getCurrentCurrency } from '../../selectors' |
||||
import * as i18nhooks from '../useI18nContext' |
||||
import { getMessage } from '../../helpers/utils/i18n-helper' |
||||
import messages from '../../../../app/_locales/en/messages.json' |
||||
|
||||
|
||||
const expectedResults = [ |
||||
{ title: 'Send ETH', |
||||
category: 'send', |
||||
subtitle: 'To: 0xffe5...1a97', |
||||
date: 'May 12', |
||||
primaryCurrency: '-1 ETH', |
||||
senderAddress: '0x9eca64466f257793eaa52fcfff5066894b76a149', |
||||
recipientAddress: '0xffe5bc4e8f1f969934d773fa67da095d2e491a97', |
||||
secondaryCurrency: '-1 ETH', |
||||
isPending: false, |
||||
status: 'confirmed' }, |
||||
{ title: 'Send ETH', |
||||
category: 'send', |
||||
subtitle: 'To: 0x0ccc...8848', |
||||
date: 'May 12', |
||||
primaryCurrency: '-2 ETH', |
||||
senderAddress: '0x9eca64466f257793eaa52fcfff5066894b76a149', |
||||
recipientAddress: '0x0ccc8aeeaf5ce790f3b448325981a143fdef8848', |
||||
secondaryCurrency: '-2 ETH', |
||||
isPending: false, |
||||
status: 'confirmed' }, |
||||
{ title: 'Send ETH', |
||||
category: 'send', |
||||
subtitle: 'To: 0xffe5...1a97', |
||||
date: 'May 12', |
||||
primaryCurrency: '-2 ETH', |
||||
senderAddress: '0x9eca64466f257793eaa52fcfff5066894b76a149', |
||||
recipientAddress: '0xffe5bc4e8f1f969934d773fa67da095d2e491a97', |
||||
secondaryCurrency: '-2 ETH', |
||||
isPending: false, |
||||
status: 'confirmed' }, |
||||
{ title: 'Receive', |
||||
category: 'receive', |
||||
subtitle: 'From: 0x31b9...4523', |
||||
date: 'May 12', |
||||
primaryCurrency: '18.75 ETH', |
||||
senderAddress: '0x31b98d14007bdee637298086988a0bbd31184523', |
||||
recipientAddress: '0x9eca64466f257793eaa52fcfff5066894b76a149', |
||||
secondaryCurrency: '18.75 ETH', |
||||
isPending: false, |
||||
status: 'confirmed' }, |
||||
{ title: 'Receive', |
||||
category: 'receive', |
||||
subtitle: 'From: 0x9eca...a149', |
||||
date: 'May 8', |
||||
primaryCurrency: '0 ETH', |
||||
senderAddress: '0x9eca64466f257793eaa52fcfff5066894b76a149', |
||||
recipientAddress: '0x9eca64466f257793eaa52fcfff5066894b76a149', |
||||
secondaryCurrency: '0 ETH', |
||||
isPending: false, |
||||
status: 'confirmed' }, |
||||
{ title: 'Receive', |
||||
category: 'receive', |
||||
subtitle: 'From: 0xee01...febb', |
||||
date: 'May 24', |
||||
primaryCurrency: '1 ETH', |
||||
senderAddress: '0xee014609ef9e09776ac5fe00bdbfef57bcdefebb', |
||||
recipientAddress: '0x9eca64466f257793eaa52fcfff5066894b76a149', |
||||
secondaryCurrency: '1 ETH', |
||||
isPending: false, |
||||
status: 'confirmed' }, |
||||
] |
||||
|
||||
let useSelector, useI18nContext |
||||
|
||||
describe('useTransactionDisplayData', function () { |
||||
before(function () { |
||||
useSelector = sinon.stub(reactRedux, 'useSelector') |
||||
useI18nContext = sinon.stub(i18nhooks, 'useI18nContext') |
||||
useI18nContext.returns((key, variables) => getMessage('en', messages, key, variables)) |
||||
useSelector.callsFake((selector) => { |
||||
if (selector === tokenSelector) { |
||||
return [] |
||||
} else if (selector === getPreferences) { |
||||
return { |
||||
useNativeCurrencyAsPrimaryCurrency: true, |
||||
} |
||||
} else if (selector === getShouldShowFiat) { |
||||
return false |
||||
} else if (selector === getNativeCurrency) { |
||||
return 'ETH' |
||||
} else if (selector === getCurrentCurrency) { |
||||
return 'ETH' |
||||
} else { |
||||
return null |
||||
} |
||||
}) |
||||
}) |
||||
transactions.forEach((transactionGroup, idx) => { |
||||
describe(`when called with group containing primaryTransaction id ${transactionGroup.primaryTransaction.id}`, function () { |
||||
const expected = expectedResults[idx] |
||||
it(`should return a title of ${expected.title}`, function () { |
||||
const { result } = renderHook(() => useTransactionDisplayData(transactionGroup)) |
||||
assert.equal(result.current.title, expected.title) |
||||
}) |
||||
it(`should return a subtitle of ${expected.subtitle}`, function () { |
||||
const { result } = renderHook(() => useTransactionDisplayData(transactionGroup)) |
||||
assert.equal(result.current.subtitle, expected.subtitle) |
||||
}) |
||||
it(`should return a category of ${expected.category}`, function () { |
||||
const { result } = renderHook(() => useTransactionDisplayData(transactionGroup)) |
||||
assert.equal(result.current.category, expected.category) |
||||
}) |
||||
it(`should return a primaryCurrency of ${expected.primaryCurrency}`, function () { |
||||
const { result } = renderHook(() => useTransactionDisplayData(transactionGroup)) |
||||
assert.equal(result.current.primaryCurrency, expected.primaryCurrency) |
||||
}) |
||||
it(`should return a secondaryCurrency of ${expected.secondaryCurrency}`, function () { |
||||
const { result } = renderHook(() => useTransactionDisplayData(transactionGroup)) |
||||
assert.equal(result.current.secondaryCurrency, expected.secondaryCurrency) |
||||
}) |
||||
it(`should return a status of ${expected.status}`, function () { |
||||
const { result } = renderHook(() => useTransactionDisplayData(transactionGroup)) |
||||
assert.equal(result.current.status, expected.status) |
||||
}) |
||||
it(`should return a recipientAddress of ${expected.recipientAddress}`, function () { |
||||
const { result } = renderHook(() => useTransactionDisplayData(transactionGroup)) |
||||
assert.equal(result.current.recipientAddress, expected.recipientAddress) |
||||
}) |
||||
it(`should return a senderAddress of ${expected.senderAddress}`, function () { |
||||
const { result } = renderHook(() => useTransactionDisplayData(transactionGroup)) |
||||
assert.equal(result.current.senderAddress, expected.senderAddress) |
||||
}) |
||||
}) |
||||
}) |
||||
it('should return an appropriate object', function () { |
||||
const { result } = renderHook(() => useTransactionDisplayData(transactions[0])) |
||||
assert.deepEqual(result.current, expectedResults[0]) |
||||
}) |
||||
after(function () { |
||||
useSelector.restore() |
||||
useI18nContext.restore() |
||||
}) |
||||
}) |
@ -0,0 +1,50 @@ |
||||
import { useDispatch, useSelector } from 'react-redux' |
||||
import { useCallback } from 'react' |
||||
import { showModal } from '../store/actions' |
||||
import { isBalanceSufficient } from '../pages/send/send.utils' |
||||
import { getHexGasTotal, increaseLastGasPrice } from '../helpers/utils/confirm-tx.util' |
||||
import { getConversionRate, getSelectedAccount } from '../selectors' |
||||
|
||||
/** |
||||
* Determine whether a transaction can be cancelled and provide a method to |
||||
* kick off the process of cancellation. |
||||
* |
||||
* Provides a reusable hook that, given a transactionGroup, will return |
||||
* whether or not the account has enough funds to cover the gas cancellation |
||||
* fee, and a method for beginning the cancellation process |
||||
* @param {Object} transactionGroup |
||||
* @return {[boolean, Function]} |
||||
*/ |
||||
export function useCancelTransaction (transactionGroup) { |
||||
const { primaryTransaction, initialTransaction } = transactionGroup |
||||
const gasPrice = primaryTransaction.txParams?.gasPrice |
||||
const id = initialTransaction.id |
||||
const dispatch = useDispatch() |
||||
const selectedAccount = useSelector(getSelectedAccount) |
||||
const conversionRate = useSelector(getConversionRate) |
||||
const showCancelModal = useCallback(() => { |
||||
return dispatch(showModal({ name: 'CANCEL_TRANSACTION', transactionId: id, originalGasPrice: gasPrice })) |
||||
}, [dispatch, id, gasPrice]) |
||||
|
||||
|
||||
const hasEnoughCancelGas = primaryTransaction.txParams && isBalanceSufficient({ |
||||
amount: '0x0', |
||||
gasTotal: getHexGasTotal({ |
||||
gasPrice: increaseLastGasPrice(gasPrice), |
||||
gasLimit: primaryTransaction.txParams.gas, |
||||
}), |
||||
balance: selectedAccount.balance, |
||||
conversionRate, |
||||
}) |
||||
|
||||
const cancelTransaction = useCallback((event) => { |
||||
event.stopPropagation() |
||||
if (!hasEnoughCancelGas) { |
||||
return |
||||
} |
||||
|
||||
showCancelModal() |
||||
}, [showCancelModal, hasEnoughCancelGas]) |
||||
|
||||
return [hasEnoughCancelGas, cancelTransaction] |
||||
} |
@ -0,0 +1,30 @@ |
||||
import { useEffect, useCallback } from 'react' |
||||
import { useDispatch, useSelector } from 'react-redux' |
||||
import { getContractMethodData as getContractMethodDataAction } from '../store/actions' |
||||
|
||||
import { getKnownMethodData } from '../selectors/selectors' |
||||
|
||||
/** |
||||
* Access known method data and attempt to resolve unknown method data |
||||
* |
||||
* encapsulates an effect that will fetch methodData when the component mounts, |
||||
* and subsequently anytime the provided data attribute changes. Note that |
||||
* the getContractMethodData action handles over-fetching prevention, first checking |
||||
* if the data is in the store and returning it directly. While using this hook |
||||
* in multiple places in a tree for the same data will create extra event ticks and |
||||
* hit the action more frequently, it should only ever result in a single store update |
||||
* @param {string} data the transaction data to find method data for |
||||
* @return {Object} contract method data |
||||
*/ |
||||
export function useMethodData (data) { |
||||
const dispatch = useDispatch() |
||||
const knownMethodData = useSelector((state) => getKnownMethodData(state, data)) |
||||
const getContractMethodData = useCallback((methodData) => dispatch(getContractMethodDataAction(methodData)), [dispatch]) |
||||
|
||||
useEffect(() => { |
||||
if (data) { |
||||
getContractMethodData(data) |
||||
} |
||||
}, [getContractMethodData, data]) |
||||
return knownMethodData |
||||
} |
@ -0,0 +1,62 @@ |
||||
import { useDispatch } from 'react-redux' |
||||
import { useCallback } from 'react' |
||||
import { setSelectedToken, showSidebar } from '../store/actions' |
||||
import { |
||||
fetchBasicGasAndTimeEstimates, |
||||
fetchGasEstimates, |
||||
setCustomGasPriceForRetry, |
||||
setCustomGasLimit, |
||||
} from '../ducks/gas/gas.duck' |
||||
import { TOKEN_METHOD_TRANSFER } from '../helpers/constants/transactions' |
||||
import { increaseLastGasPrice } from '../helpers/utils/confirm-tx.util' |
||||
import { useMetricEvent } from './useMetricEvent' |
||||
import { useMethodData } from './useMethodData' |
||||
|
||||
|
||||
/** |
||||
* Provides a reusable hook that, given a transactionGroup, will return |
||||
* a method for beginning the retry process |
||||
* @param {Object} transactionGroup - the transaction group |
||||
* @return {Function} |
||||
*/ |
||||
export function useRetryTransaction (transactionGroup) { |
||||
const { primaryTransaction, initialTransaction } = transactionGroup |
||||
const gasPrice = primaryTransaction.txParams?.gasPrice |
||||
const methodData = useMethodData(primaryTransaction.txParams?.data) |
||||
const trackMetricsEvent = useMetricEvent(({ |
||||
eventOpts: { |
||||
category: 'Navigation', |
||||
action: 'Activity Log', |
||||
name: 'Clicked "Speed Up"', |
||||
}, |
||||
})) |
||||
const dispatch = useDispatch() |
||||
|
||||
const { name: methodName } = methodData || {} |
||||
|
||||
const retryTransaction = useCallback(async (event) => { |
||||
event.stopPropagation() |
||||
|
||||
trackMetricsEvent() |
||||
const basicEstimates = await dispatch(fetchBasicGasAndTimeEstimates) |
||||
await dispatch(fetchGasEstimates(basicEstimates.blockTime)) |
||||
const transaction = initialTransaction |
||||
const increasedGasPrice = increaseLastGasPrice(gasPrice) |
||||
dispatch(setCustomGasPriceForRetry(increasedGasPrice || transaction.txParams?.gasPrice)) |
||||
dispatch(setCustomGasLimit(transaction.txParams?.gas)) |
||||
dispatch(showSidebar({ |
||||
transitionName: 'sidebar-left', |
||||
type: 'customize-gas', |
||||
props: { transaction }, |
||||
})) |
||||
|
||||
if ( |
||||
methodName === TOKEN_METHOD_TRANSFER && |
||||
initialTransaction.txParams.to |
||||
) { |
||||
dispatch(setSelectedToken(initialTransaction.txParams.to)) |
||||
} |
||||
}, [dispatch, methodName, trackMetricsEvent, initialTransaction, gasPrice]) |
||||
|
||||
return retryTransaction |
||||
} |
@ -0,0 +1,46 @@ |
||||
import { useEffect, useState } from 'react' |
||||
|
||||
/** |
||||
* Evaluates whether the transaction is eligible to be sped up, and registers |
||||
* an effect to check the logic again after the transaction has surpassed 5 seconds |
||||
* of queue time. |
||||
* @param {Object} transactionGroup - the transaction group to check against |
||||
* @param {boolean} isEarliestNonce - Whether this group is currently the earliest nonce |
||||
*/ |
||||
export function useShouldShowSpeedUp (transactionGroup, isEarliestNonce) { |
||||
const { transactions, hasRetried } = transactionGroup |
||||
const [earliestTransaction = {}] = transactions |
||||
const { submittedTime } = earliestTransaction |
||||
const [speedUpEnabled, setSpeedUpEnabled] = useState(() => { |
||||
return Date.now() - submittedTime > 5000 && isEarliestNonce && !hasRetried |
||||
}) |
||||
useEffect(() => { |
||||
// because this hook is optimized to only run on changes we have to
|
||||
// key into the changing time delta between submittedTime and now()
|
||||
// and if the status of the transaction changes based on that difference
|
||||
// trigger a setState call to tell react to re-render. This effect will
|
||||
// also immediately set retryEnabled and not create a timeout if the
|
||||
// condition is already met. This effect will run anytime the variables
|
||||
// for determining enabled status change
|
||||
let timeoutId |
||||
if (!hasRetried && isEarliestNonce && !speedUpEnabled) { |
||||
if (Date.now() - submittedTime > 5000) { |
||||
setSpeedUpEnabled(true) |
||||
} else { |
||||
timeoutId = setTimeout(() => { |
||||
setSpeedUpEnabled(true) |
||||
clearTimeout(timeoutId) |
||||
}, 5001 - (Date.now() - submittedTime)) |
||||
} |
||||
} |
||||
// Anytime the effect is re-ran, make sure to remove a previously set timeout
|
||||
// so as to avoid multiple timers potentially overlapping
|
||||
return () => { |
||||
if (timeoutId) { |
||||
clearTimeout(timeoutId) |
||||
} |
||||
} |
||||
}, [submittedTime, hasRetried, isEarliestNonce]) |
||||
|
||||
return speedUpEnabled |
||||
} |
@ -0,0 +1,23 @@ |
||||
import { useMemo } from 'react' |
||||
import { getTokenData } from '../helpers/utils/transactions.util' |
||||
|
||||
/** |
||||
* useTokenData |
||||
* Given the data string from txParams return a decoded object of the details of the |
||||
* transaction data. |
||||
* @param {string} [transactionData] - Raw data string from token transaction |
||||
* @param {boolean} [isTokenTransaction] - Due to the nature of hooks, it isn't possible |
||||
* to conditionally call this hook. This flag will |
||||
* force this hook to return null if it set as false |
||||
* which indicates the transaction is not associated |
||||
* with a token. |
||||
* @return {Object} - Decoded token data |
||||
*/ |
||||
export function useTokenData (transactionData, isTokenTransaction = true) { |
||||
return useMemo(() => { |
||||
if (!isTokenTransaction || !transactionData) { |
||||
return null |
||||
} |
||||
return getTokenData(transactionData) |
||||
}, [isTokenTransaction, transactionData]) |
||||
} |
@ -0,0 +1,153 @@ |
||||
import { useSelector } from 'react-redux' |
||||
import { getKnownMethodData } from '../selectors/selectors' |
||||
import { getTransactionActionKey, getStatusKey } from '../helpers/utils/transactions.util' |
||||
import { camelCaseToCapitalize } from '../helpers/utils/common.util' |
||||
import { useI18nContext } from './useI18nContext' |
||||
import { PRIMARY, SECONDARY } from '../helpers/constants/common' |
||||
import { getTokenToAddress } from '../helpers/utils/token-util' |
||||
import { useUserPreferencedCurrency } from './useUserPreferencedCurrency' |
||||
import { formatDateWithYearContext, shortenAddress } from '../helpers/utils/util' |
||||
import { |
||||
CONTRACT_INTERACTION_KEY, |
||||
DEPLOY_CONTRACT_ACTION_KEY, |
||||
INCOMING_TRANSACTION, |
||||
TOKEN_METHOD_TRANSFER, |
||||
TOKEN_METHOD_TRANSFER_FROM, |
||||
SEND_ETHER_ACTION_KEY, |
||||
TRANSACTION_CATEGORY_APPROVAL, |
||||
TRANSACTION_CATEGORY_INTERACTION, |
||||
TRANSACTION_CATEGORY_RECEIVE, |
||||
TRANSACTION_CATEGORY_SEND, |
||||
TRANSACTION_CATEGORY_SIGNATURE_REQUEST, |
||||
TOKEN_METHOD_APPROVE, |
||||
PENDING_STATUS_HASH, |
||||
TOKEN_CATEGORY_HASH, |
||||
} from '../helpers/constants/transactions' |
||||
import { useCurrencyDisplay } from './useCurrencyDisplay' |
||||
import { useTokenDisplayValue } from './useTokenDisplayValue' |
||||
import { useTokenData } from './useTokenData' |
||||
import { tokenSelector } from '../selectors' |
||||
|
||||
/** |
||||
* @typedef {Object} TransactionDisplayData |
||||
* @property {string} title - primary description of the transaction |
||||
* @property {string} subtitle - supporting text describing the transaction |
||||
* @property {string} category - the transaction category |
||||
* @property {string} primaryCurrency - the currency string to display in the primary position |
||||
* @property {string} [secondaryCurrency] - the currency string to display in the secondary position |
||||
* @property {string} status - the status of the transaction |
||||
* @property {string} senderAddress - the Ethereum address of the sender |
||||
* @property {string} recipientAddress - the Ethereum address of the recipient |
||||
*/ |
||||
|
||||
/** |
||||
* Get computed values used for displaying transaction data to a user |
||||
* |
||||
* The goal of this method is to perform all of the necessary computation and |
||||
* state access required to take a transactionGroup and derive from it a shape |
||||
* of data that can power all views related to a transaction. Presently the main |
||||
* case is for shared logic between transaction-list-item and transaction-detail-view |
||||
* @param {Object} transactionGroup group of transactions |
||||
* @return {TransactionDisplayData} |
||||
*/ |
||||
export function useTransactionDisplayData (transactionGroup) { |
||||
const knownTokens = useSelector(tokenSelector) |
||||
const t = useI18nContext() |
||||
const { initialTransaction, primaryTransaction } = transactionGroup |
||||
// initialTransaction contains the data we need to derive the primary purpose of this transaction group
|
||||
const { transactionCategory } = initialTransaction |
||||
|
||||
const { from: senderAddress, to } = initialTransaction.txParams || {} |
||||
|
||||
// for smart contract interactions, methodData can be used to derive the name of the action being taken
|
||||
const methodData = useSelector((state) => getKnownMethodData(state, initialTransaction?.txParams?.data)) || {} |
||||
|
||||
const actionKey = getTransactionActionKey(initialTransaction) |
||||
const status = getStatusKey(primaryTransaction) |
||||
|
||||
const primaryValue = primaryTransaction.txParams?.value |
||||
let prefix = '-' |
||||
const date = formatDateWithYearContext(initialTransaction.time || 0) |
||||
let subtitle |
||||
let recipientAddress = to |
||||
|
||||
// This value is used to determine whether we should look inside txParams.data
|
||||
// to pull out and render token related information
|
||||
const isTokenCategory = TOKEN_CATEGORY_HASH[transactionCategory] |
||||
|
||||
// these values are always instantiated because they are either
|
||||
// used by or returned from hooks. Hooks must be called at the top level,
|
||||
// so as an additional safeguard against inappropriately associating token
|
||||
// transfers, we pass an additional argument to these hooks that will be
|
||||
// false for non-token transactions. This additional argument forces the
|
||||
// hook to return null
|
||||
const token = isTokenCategory && knownTokens.find((token) => token.address === recipientAddress) |
||||
const tokenData = useTokenData(initialTransaction?.txParams?.data, isTokenCategory) |
||||
const tokenDisplayValue = useTokenDisplayValue(initialTransaction?.txParams?.data, token, isTokenCategory) |
||||
|
||||
let category |
||||
let title |
||||
// There are four types of transaction entries that are currently differentiated in the design
|
||||
// 1. (PENDING DESIGN) signature request
|
||||
// 2. Send (sendEth sendTokens)
|
||||
// 3. Deposit
|
||||
// 4. Site interaction
|
||||
// 5. Approval
|
||||
if (transactionCategory == null) { |
||||
const origin = initialTransaction.msgParams?.origin || initialTransaction.origin |
||||
category = TRANSACTION_CATEGORY_SIGNATURE_REQUEST |
||||
title = t('signatureRequest') |
||||
subtitle = origin || '' |
||||
} else if (transactionCategory === TOKEN_METHOD_APPROVE) { |
||||
category = TRANSACTION_CATEGORY_APPROVAL |
||||
title = t('approve') |
||||
subtitle = initialTransaction.origin |
||||
} else if (transactionCategory === DEPLOY_CONTRACT_ACTION_KEY || transactionCategory === CONTRACT_INTERACTION_KEY) { |
||||
category = TRANSACTION_CATEGORY_INTERACTION |
||||
title = (methodData?.name && camelCaseToCapitalize(methodData.name)) || (actionKey && t(actionKey)) || '' |
||||
subtitle = initialTransaction.origin |
||||
} else if (transactionCategory === INCOMING_TRANSACTION) { |
||||
category = TRANSACTION_CATEGORY_RECEIVE |
||||
title = t('receive') |
||||
prefix = '' |
||||
subtitle = t('fromAddress', [shortenAddress(senderAddress)]) |
||||
} else if (transactionCategory === TOKEN_METHOD_TRANSFER_FROM || transactionCategory === TOKEN_METHOD_TRANSFER) { |
||||
category = TRANSACTION_CATEGORY_SEND |
||||
title = t('sendSpecifiedTokens', [token?.symbol || t('token')]) |
||||
recipientAddress = getTokenToAddress(tokenData.params) |
||||
subtitle = t('toAddress', [shortenAddress(recipientAddress)]) |
||||
} else if (transactionCategory === SEND_ETHER_ACTION_KEY) { |
||||
category = TRANSACTION_CATEGORY_SEND |
||||
title = t('sendETH') |
||||
subtitle = t('toAddress', [shortenAddress(recipientAddress)]) |
||||
} |
||||
|
||||
const primaryCurrencyPreferences = useUserPreferencedCurrency(PRIMARY) |
||||
const secondaryCurrencyPreferences = useUserPreferencedCurrency(SECONDARY) |
||||
|
||||
const [primaryCurrency] = useCurrencyDisplay(primaryValue, { |
||||
prefix, |
||||
displayValue: isTokenCategory && tokenDisplayValue, |
||||
suffix: isTokenCategory && token?.symbol, |
||||
...primaryCurrencyPreferences, |
||||
}) |
||||
|
||||
const [secondaryCurrency] = useCurrencyDisplay(primaryValue, { |
||||
prefix, |
||||
displayValue: isTokenCategory && tokenDisplayValue, |
||||
...secondaryCurrencyPreferences, |
||||
}) |
||||
|
||||
return { |
||||
title, |
||||
category, |
||||
date, |
||||
subtitle, |
||||
primaryCurrency, |
||||
senderAddress, |
||||
recipientAddress, |
||||
secondaryCurrency: isTokenCategory ? undefined : secondaryCurrency, |
||||
status, |
||||
isPending: status in PENDING_STATUS_HASH, |
||||
} |
||||
} |
Loading…
Reference in new issue