1
0
mirror of https://github.com/kremalicious/metamask-extension.git synced 2024-12-23 01:39:44 +01:00

Implement new transaction list design (#8564)

Co-authored-by: Whymarrh Whitby <whymarrh.whitby@gmail.com>
Co-authored-by: Mark Stacey <markjstacey@gmail.com>
This commit is contained in:
Brad Decker 2020-05-26 15:49:11 -05:00 committed by GitHub
parent e06fb2c9f6
commit 706dc02cb4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
42 changed files with 1909 additions and 860 deletions

View File

@ -226,7 +226,7 @@
"description": "The name of the application"
},
"approve": {
"message": "Approve"
"message": "Approve spend limit"
},
"approved": {
"message": "Approved"
@ -663,6 +663,10 @@
"from": {
"message": "From"
},
"fromAddress": {
"message": "From: $1",
"description": "$1 is the address to include in the From label. It is typically shortened first using shortenAddress"
},
"functionApprove": {
"message": "Function: Approve"
},
@ -1117,12 +1121,18 @@
"queue": {
"message": "Queue"
},
"queued": {
"message": "Queued"
},
"readdToken": {
"message": "You can add this token back in the future by going to “Add token” in your accounts options menu."
},
"recents": {
"message": "Recents"
},
"receive": {
"message": "Receive"
},
"recipientAddress": {
"message": "Recipient Address"
},
@ -1301,6 +1311,10 @@
"sentTokens": {
"message": "sent tokens"
},
"sendSpecifiedTokens": {
"message": "Send $1",
"description": "Symbol of the specified token"
},
"separateEachWord": {
"message": "Separate each word with a single space"
},
@ -1507,6 +1521,10 @@
"to": {
"message": "To"
},
"toAddress": {
"message": "To: $1",
"description": "$1 is the address to include in the To label. It is typically shortened first using shortenAddress"
},
"toWithColon": {
"message": "To:"
},

View File

@ -67,7 +67,17 @@
"name": "Address Book Account 1"
}
],
"tokens": [],
"tokens": [{
"name": "FakeTokenOne",
"address": "0x66f30b996a7d345cd00badcfe75e81e25dc5e1ec",
"symbol": "FTO",
"decimals": 2
}, {
"name": "FakeTokenTwo",
"address": "0x66f30b996a7d345cd00badcfe75e81e25dc5e1eb",
"symbol": "FTT",
"decimals": 2
}],
"transactions": {},
"incomingTransactions": {},
"currentNetworkTxList": [
@ -208,6 +218,7 @@
"rawTx": "0xf8610384773594008094f45d68f31b3c9ac84ff0d07b86c59b753a60b1e3808029a052e5246c9a404f756a246b8cec545099741aeb4e6e0add935a5b7a366fa88f95a0538eaa2421e50377c534244dcdcd15ace00bf9c0adbd9eb162baae2b9e89a36f",
"status": "failed",
"time": 1522378334455,
"transactionCategory": "sentEther",
"txParams": {
"chainId": "0x3",
"from": "0xd85a4b6a394794842887b8284293d69163007bbb",
@ -224,6 +235,7 @@
"status": "approved",
"metamaskNetworkId": "1",
"loadingDefaults": false,
"transactionCategory": "sentEther",
"txParams": {
"from": "0xd85a4b6a394794842887b8284293d69163007bbb",
"to": "0xf45d68f31b3c9ac84ff0d07b86c59b753a60b1e3",
@ -451,6 +463,7 @@
"status": "confirmed",
"submittedTime": 1522346282571,
"time": 1522348270251,
"transactionCategory": "transfer",
"txParams": {
"chainId": "0x3",
"data": "0xa9059cbb000000000000000000000000e7884118ee52ec3f4eef715cb022279d7d4181a9000000000000000000000000000000000000000000000000000000000000000b",
@ -641,6 +654,7 @@
"status": "confirmed",
"submittedTime": 1522346282571,
"time": 1522346270251,
"transactionCategory": "transfer",
"txParams": {
"chainId": "0x3",
"data": "0xa9059cbb000000000000000000000000e7884118ee52ec3f4eef715cb022279d7d4181a9000000000000000000000000000000000000000000000000000000000000000b",
@ -658,6 +672,7 @@
"status": "submitted",
"metamaskNetworkId": "1",
"loadingDefaults": false,
"transactionCategory": "sentEther",
"txParams": {
"from": "0xd85a4b6a394794842887b8284293d69163007bbb",
"to": "0xf45d68f31b3c9ac84ff0d07b86c59b753a60b1e3",
@ -819,6 +834,7 @@
"status": "unapproved",
"metamaskNetworkId": "1",
"loadingDefaults": false,
"transactionCategory": "sentEther",
"txParams": {
"from": "0xd85a4b6a394794842887b8284293d69163007bbb",
"to": "0xf45d68f31b3c9ac84ff0d07b86c59b753a60b1e3",
@ -858,6 +874,7 @@
"status": "unapproved",
"metamaskNetworkId": "1",
"loadingDefaults": false,
"transactionCategory": "sentEther",
"txParams": {
"from": "0x5b1cbd5636d484bf1cb6927a9425db9e7dc73ce4",
"to": "0xf45d68f31b3c9ac84ff0d07b86c59b753a60b1e3",

View File

@ -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
}
]

View File

@ -200,7 +200,7 @@ describe('MetaMask', function () {
return confirmedTxes.length === 1
}, 10000)
const txValues = await driver.findElement(By.css('.transaction-list-item__amount--primary'))
const txValues = await driver.findElement(By.css('.transaction-list-item__primary-currency'))
await driver.wait(until.elementTextMatches(txValues, /-1\s*ETH/), 10000)
})
})
@ -237,7 +237,7 @@ describe('MetaMask', function () {
return confirmedTxes.length === 2
}, 10000)
const txValues = await driver.findElement(By.css('.transaction-list-item__amount--primary'))
const txValues = await driver.findElement(By.css('.transaction-list-item__primary-currency'))
await driver.wait(until.elementTextMatches(txValues, /-2\s*ETH/), 10000)
})
})

View File

@ -219,7 +219,7 @@ describe('Using MetaMask with an existing account', function () {
return confirmedTxes.length === 1
}, 10000)
const txValues = await driver.findElements(By.css('.transaction-list-item__amount--primary'))
const txValues = await driver.findElements(By.css('.transaction-list-item__primary-currency'))
assert.equal(txValues.length, 1)
assert.ok(/-1\s*ETH/.test(await txValues[0].getText()))
})

View File

@ -214,7 +214,7 @@ describe('MetaMask', function () {
return confirmedTxes.length === 1
}, 10000)
const txValues = await driver.findElement(By.css('.transaction-list-item__amount--primary'))
const txValues = await driver.findElement(By.css('.transaction-list-item__primary-currency'))
await driver.wait(until.elementTextMatches(txValues, /-1\s*ETH/), 10000)
})
})

View File

@ -270,7 +270,7 @@ describe('MetaMask', function () {
return confirmedTxes.length === 1
}, 10000)
const txValues = await driver.findElement(By.css('.transaction-list-item__amount--primary'))
const txValues = await driver.findElement(By.css('.transaction-list-item__primary-currency'))
await driver.wait(until.elementTextMatches(txValues, /-1\s*ETH/), 10000)
})
})
@ -309,7 +309,7 @@ describe('MetaMask', function () {
return confirmedTxes.length === 2
}, 10000)
const txValues = await driver.findElement(By.css('.transaction-list-item__amount--primary'))
const txValues = await driver.findElement(By.css('.transaction-list-item__primary-currency'))
await driver.wait(until.elementTextMatches(txValues, /-1\s*ETH/), 10000)
})
})
@ -357,7 +357,7 @@ describe('MetaMask', function () {
return confirmedTxes.length === 3
}, 10000)
const txValues = await driver.findElement(By.css('.transaction-list-item__amount--primary'))
const txValues = await driver.findElement(By.css('.transaction-list-item__primary-currency'))
await driver.wait(until.elementTextMatches(txValues, /-1\s*ETH/), 10000)
})
})
@ -462,18 +462,19 @@ describe('MetaMask', function () {
return confirmedTxes.length === 4
}, 10000)
const txValue = await driver.findClickableElement(By.css('.transaction-list-item__amount--primary'))
const txValue = await driver.findClickableElement(By.css('.transaction-list-item__primary-currency'))
await driver.wait(until.elementTextMatches(txValue, /-3\s*ETH/), 10000)
})
it('the transaction has the expected gas price', async function () {
const txValue = await driver.findClickableElement(By.css('.transaction-list-item__amount--primary'))
const txValue = await driver.findClickableElement(By.css('.transaction-list-item__primary-currency'))
await txValue.click()
const popoverCloseButton = await driver.findClickableElement(By.css('.popover-header__button'))
const txGasPrices = await driver.findElements(By.css('.transaction-breakdown__value'))
const txGasPriceLabels = await driver.findElements(By.css('.transaction-breakdown-row__title'))
await driver.wait(until.elementTextMatches(txGasPrices[3], /^10$/), 10000)
assert(txGasPriceLabels[2])
await txValue.click()
await popoverCloseButton.click()
})
})
@ -624,7 +625,7 @@ describe('MetaMask', function () {
await driver.switchToWindow(extension)
await driver.delay(regularDelayMs)
await driver.clickElement(By.xpath(`//div[contains(text(), 'Contract Deployment')]`))
await driver.clickElement(By.xpath(`//h2[contains(text(), 'Contract Deployment')]`))
await driver.delay(largeDelayMs)
})
@ -654,7 +655,7 @@ describe('MetaMask', function () {
return confirmedTxes.length === 6
}, 10000)
const txAction = await driver.findElements(By.css('.transaction-list-item__action'))
const txAction = await driver.findElements(By.css('.list-item__heading'))
await driver.wait(until.elementTextMatches(txAction[0], /Contract\sDeployment/), 10000)
await driver.delay(regularDelayMs)
})
@ -676,7 +677,7 @@ describe('MetaMask', function () {
await driver.delay(largeDelayMs * 2)
await driver.findElements(By.css('.transaction-list-item'))
const txListValue = await driver.findClickableElement(By.css('.transaction-list-item__amount--primary'))
const txListValue = await driver.findClickableElement(By.css('.transaction-list-item__primary-currency'))
await driver.wait(until.elementTextMatches(txListValue, /-4\s*ETH/), 10000)
await txListValue.click()
await driver.delay(regularDelayMs)
@ -718,7 +719,7 @@ describe('MetaMask', function () {
return confirmedTxes.length === 7
}, 10000)
const txValues = await driver.findElements(By.css('.transaction-list-item__amount--primary'))
const txValues = await driver.findElements(By.css('.transaction-list-item__primary-currency'))
await driver.wait(until.elementTextMatches(txValues[0], /-4\s*ETH/), 10000)
})
@ -743,7 +744,7 @@ describe('MetaMask', function () {
return confirmedTxes.length === 8
}, 10000)
const txValues = await driver.findElement(By.css('.transaction-list-item__amount--primary'))
const txValues = await driver.findElement(By.css('.transaction-list-item__primary-currency'))
await driver.wait(until.elementTextMatches(txValues, /-0\s*ETH/), 10000)
await driver.closeAllWindowHandlesExcept([extension, dapp])
@ -904,12 +905,12 @@ describe('MetaMask', function () {
return confirmedTxes.length === 1
}, 10000)
const txValues = await driver.findElements(By.css('.transaction-list-item__amount--primary'))
const txValues = await driver.findElements(By.css('.transaction-list-item__primary-currency'))
assert.equal(txValues.length, 1)
await driver.wait(until.elementTextMatches(txValues[0], /-1\s*TST/), 10000)
const txStatuses = await driver.findElements(By.css('.transaction-list-item__action'))
await driver.wait(until.elementTextMatches(txStatuses[0], /Sent\sToken/i), 10000)
const txStatuses = await driver.findElements(By.css('.list-item__heading'))
await driver.wait(until.elementTextMatches(txStatuses[0], /Send\sTST/i), 10000)
})
})
@ -930,7 +931,7 @@ describe('MetaMask', function () {
await driver.delay(largeDelayMs)
await driver.findElements(By.css('.transaction-list__pending-transactions'))
const txListValue = await driver.findClickableElement(By.css('.transaction-list-item__amount--primary'))
const txListValue = await driver.findClickableElement(By.css('.transaction-list-item__primary-currency'))
await driver.wait(until.elementTextMatches(txListValue, /-1.5\s*TST/), 10000)
await txListValue.click()
await driver.delay(regularDelayMs)
@ -986,10 +987,10 @@ describe('MetaMask', function () {
return confirmedTxes.length === 2
}, 10000)
const txValues = await driver.findElements(By.css('.transaction-list-item__amount--primary'))
const txValues = await driver.findElements(By.css('.transaction-list-item__primary-currency'))
await driver.wait(until.elementTextMatches(txValues[0], /-1.5\s*TST/))
const txStatuses = await driver.findElements(By.css('.transaction-list-item__action'))
await driver.wait(until.elementTextMatches(txStatuses[0], /Sent\sToken/), 10000)
const txStatuses = await driver.findElements(By.css('.list-item__heading'))
await driver.wait(until.elementTextMatches(txStatuses[0], /Send\sTST/), 10000)
await driver.clickElement(By.css('[data-testid="wallet-balance"]'))
@ -1023,7 +1024,7 @@ describe('MetaMask', function () {
return pendingTxes.length === 1
}, 10000)
const [txListValue] = await driver.findElements(By.css('.transaction-list-item__amount--primary'))
const [txListValue] = await driver.findElements(By.css('.transaction-list-item__primary-currency'))
await driver.wait(until.elementTextMatches(txListValue, /-7\s*TST/))
await driver.clickElement(By.css('.transaction-list-item'))
await driver.delay(regularDelayMs)
@ -1109,9 +1110,9 @@ describe('MetaMask', function () {
return confirmedTxes.length === 3
}, 10000)
const txValues = await driver.findElements(By.css('.transaction-list-item__amount--primary'))
const txValues = await driver.findElements(By.css('.transaction-list-item__primary-currency'))
await driver.wait(until.elementTextMatches(txValues[0], /-5\s*TST/))
const txStatuses = await driver.findElements(By.css('.transaction-list-item__action'))
const txStatuses = await driver.findElements(By.css('.list-item__heading'))
await driver.wait(until.elementTextMatches(txStatuses[0], /Approve/))
})
})
@ -1136,7 +1137,7 @@ describe('MetaMask', function () {
return pendingTxes.length === 1
}, 10000)
const [txListValue] = await driver.findElements(By.css('.transaction-list-item__amount--primary'))
const [txListValue] = await driver.findElements(By.css('.transaction-list-item__primary-currency'))
await driver.wait(until.elementTextMatches(txListValue, /-1.5\s*TST/))
await driver.clickElement(By.css('.transaction-list-item'))
await driver.delay(regularDelayMs)
@ -1154,10 +1155,10 @@ describe('MetaMask', function () {
return confirmedTxes.length === 4
}, 10000)
const txValues = await driver.findElements(By.css('.transaction-list-item__amount--primary'))
const txValues = await driver.findElements(By.css('.transaction-list-item__primary-currency'))
await driver.wait(until.elementTextMatches(txValues[0], /-1.5\s*TST/))
const txStatuses = await driver.findElements(By.css('.transaction-list-item__action'))
await driver.wait(until.elementTextMatches(txStatuses[0], /Sent Tokens/))
const txStatuses = await driver.findElements(By.css('.list-item__heading'))
await driver.wait(until.elementTextMatches(txStatuses[0], /Send TST/))
})
})
@ -1182,7 +1183,7 @@ describe('MetaMask', function () {
return pendingTxes.length === 1
}, 10000)
const [txListValue] = await driver.findElements(By.css('.transaction-list-item__amount--primary'))
const [txListValue] = await driver.findElements(By.css('.transaction-list-item__primary-currency'))
await driver.wait(until.elementTextMatches(txListValue, /-7\s*TST/))
await driver.clickElement(By.css('.transaction-list-item'))
await driver.delay(regularDelayMs)
@ -1209,9 +1210,9 @@ describe('MetaMask', function () {
return confirmedTxes.length === 5
}, 10000)
const txValues = await driver.findElements(By.css('.transaction-list-item__amount--primary'))
const txValues = await driver.findElements(By.css('.transaction-list-item__primary-currency'))
await driver.wait(until.elementTextMatches(txValues[0], /-7\s*TST/))
const txStatuses = await driver.findElements(By.css('.transaction-list-item__action'))
const txStatuses = await driver.findElements(By.css('.list-item__heading'))
await driver.wait(until.elementTextMatches(txStatuses[0], /Approve/))
})
})

View File

@ -202,7 +202,7 @@ describe('Using MetaMask with an existing account', function () {
return confirmedTxes.length === 1
}, 10000)
const txValues = await driver.findElements(By.css('.transaction-list-item__amount--primary'))
const txValues = await driver.findElements(By.css('.transaction-list-item__primary-currency'))
assert.equal(txValues.length, 1)
assert.ok(/-2.2\s*ETH/.test(await txValues[0].getText()))
})

View File

@ -46,18 +46,22 @@ async function runTxListItemsTest (assert) {
assert.equal(txListItems.length, 6, 'all tx list items are rendered')
const unapprovedMsg = txListItems[0]
const unapprovedMsgDescription = await findAsync($(unapprovedMsg), '.transaction-list-item__action')
assert.equal(unapprovedMsgDescription[0].textContent, 'Signature Request', 'unapprovedMsg has correct description')
const unapprovedMsgDescription = await findAsync($(unapprovedMsg), '.transaction-list-item__status--unapproved')
assert.equal(unapprovedMsgDescription[0].textContent, 'Unapproved', 'unapprovedMsg has correct description')
const approvedTx = txListItems[2]
const approvedTxRenderedStatus = await findAsync($(approvedTx), '.transaction-list-item__status')
assert.equal(approvedTxRenderedStatus[0].textContent, 'pending', 'approvedTx has correct label')
const approvedTxRenderedStatus = await findAsync($(approvedTx), '.transaction-list-item__status--queued')
assert.equal(approvedTxRenderedStatus[0].textContent, 'Queued', 'approvedTx has correct label')
const confirmedTokenTx1 = txListItems[4]
const confirmedTokenTx1Address = await findAsync($(confirmedTokenTx1), '.transaction-list-item__status')
assert.equal(confirmedTokenTx1Address[0].textContent, 'Confirmed', 'confirmedTokenTx has correct status')
const confirmedTokenTx1Token = await findAsync($(confirmedTokenTx1), '.list-item__heading')
const confirmedTokenTx1Address = await findAsync($(confirmedTokenTx1), '.list-item__subheading')
assert.equal(confirmedTokenTx1Token[0].textContent, 'Send FTO ', 'Confirm token symbol is correct')
assert.equal(confirmedTokenTx1Address[0].textContent, 'Mar 29, 2018 · To: 0xe788...81a9', 'confirmedTokenTx has correct status')
const confirmedTokenTx2 = txListItems[5]
const confirmedTokenTx2Address = await findAsync($(confirmedTokenTx2), '.transaction-list-item__status')
assert.equal(confirmedTokenTx2Address[0].textContent, 'Confirmed', 'confirmedTokenTx has correct status')
const confirmedTokenTx2Address = await findAsync($(confirmedTokenTx2), '.list-item__subheading')
const confirmedTokenTx2Token = await findAsync($(confirmedTokenTx2), '.list-item__heading')
assert.equal(confirmedTokenTx2Token[0].textContent, 'Send FTT ', 'Confirm token symbol is correct')
assert.equal(confirmedTokenTx2Address[0].textContent, 'Mar 29, 2018 · To: 0xe788...81a9', 'confirmedTokenTx has correct status')
}

View File

@ -30,29 +30,20 @@
&__cards-container {
display: flex;
flex-direction: row;
@media screen and (max-width: $break-small) {
flex-direction: column;
}
flex-direction: column;
}
&__transaction-breakdown {
flex: 1;
margin-right: 8px;
min-width: 0;
margin: 0 0 8px 0;
@media screen and (max-width: $break-small) {
margin: 0 0 8px 0;
}
}
&__transaction-activity-log {
flex: 2;
min-width: 0;
@media screen and (min-width: $break-large) {
padding-left: 12px;
}
padding-left: 12px;
}
}

View File

@ -31,6 +31,7 @@ describe('TransactionListItemDetails Component', function () {
const wrapper = shallow(
<TransactionListItemDetails
title="Test Transaction Details"
recipientAddress="0x1"
senderAddress="0x2"
tryReverseResolveAddress={() => {}}
@ -40,12 +41,12 @@ describe('TransactionListItemDetails Component', function () {
/>,
{ context: { t: (str1, str2) => (str2 ? str1 + str2 : str1) } }
)
assert.ok(wrapper.hasClass('transaction-list-item-details'))
assert.equal(wrapper.find(Button).length, 2)
assert.equal(wrapper.find(SenderToRecipient).length, 1)
assert.equal(wrapper.find(TransactionBreakdown).length, 1)
assert.equal(wrapper.find(TransactionActivityLog).length, 1)
const child = wrapper.childAt(0)
assert.ok(child.hasClass('transaction-list-item-details'))
assert.equal(child.find(Button).length, 2)
assert.equal(child.find(SenderToRecipient).length, 1)
assert.equal(child.find(TransactionBreakdown).length, 1)
assert.equal(child.find(TransactionActivityLog).length, 1)
})
it('should render a retry button', function () {
@ -85,8 +86,10 @@ describe('TransactionListItemDetails Component', function () {
{ context: { t: (str1, str2) => (str2 ? str1 + str2 : str1) } }
)
assert.ok(wrapper.hasClass('transaction-list-item-details'))
assert.equal(wrapper.find(Button).length, 3)
const child = wrapper.childAt(0)
assert.ok(child.hasClass('transaction-list-item-details'))
assert.equal(child.find(Button).length, 3)
})
it('should disable the Copy Tx ID and View In Etherscan buttons when tx hash is missing', function () {
@ -122,8 +125,10 @@ describe('TransactionListItemDetails Component', function () {
{ context: { t: (str1, str2) => (str2 ? str1 + str2 : str1) } }
)
assert.ok(wrapper.hasClass('transaction-list-item-details'))
const buttons = wrapper.find(Button)
const child = wrapper.childAt(0)
assert.ok(child.hasClass('transaction-list-item-details'))
const buttons = child.find(Button)
assert.strictEqual(buttons.at(0).prop('disabled'), true)
assert.strictEqual(buttons.at(1).prop('disabled'), true)
})
@ -162,8 +167,10 @@ describe('TransactionListItemDetails Component', function () {
{ context: { t: (str1, str2) => (str2 ? str1 + str2 : str1) } }
)
assert.ok(wrapper.hasClass('transaction-list-item-details'))
const buttons = wrapper.find(Button)
const child = wrapper.childAt(0)
assert.ok(child.hasClass('transaction-list-item-details'))
const buttons = child.find(Button)
assert.strictEqual(buttons.at(0).prop('disabled'), false)
assert.strictEqual(buttons.at(1).prop('disabled'), false)
})

View File

@ -11,6 +11,7 @@ import TransactionBreakdown from '../transaction-breakdown'
import Button from '../../ui/button'
import Tooltip from '../../ui/tooltip'
import Copy from '../../ui/icon/copy-icon.component'
import Popover from '../../ui/popover'
export default class TransactionListItemDetails extends PureComponent {
static contextTypes = {
@ -31,6 +32,8 @@ export default class TransactionListItemDetails extends PureComponent {
isEarliestNonce: PropTypes.bool,
cancelDisabled: PropTypes.bool,
transactionGroup: PropTypes.object,
title: PropTypes.string.isRequired,
onClose: PropTypes.func.isRequired,
recipientEns: PropTypes.string,
recipientAddress: PropTypes.string.isRequired,
rpcPrefs: PropTypes.object,
@ -150,108 +153,112 @@ export default class TransactionListItemDetails extends PureComponent {
senderAddress,
isEarliestNonce,
senderNickname,
title,
onClose,
recipientNickname,
} = this.props
const { primaryTransaction: transaction } = transactionGroup
const { hash } = transaction
return (
<div className="transaction-list-item-details">
<div className="transaction-list-item-details__header">
<div>{ t('details') }</div>
<div className="transaction-list-item-details__header-buttons">
{
showSpeedUp && (
<Button
type="raised"
onClick={this.handleRetry}
className="transaction-list-item-details__header-button"
>
{ t('speedUp') }
</Button>
)
}
{ this.renderCancel() }
<Tooltip title={justCopied ? t('copiedTransactionId') : t('copyTransactionId')}>
<Button
type="raised"
onClick={this.handleCopyTxId}
className="transaction-list-item-details__header-button"
disabled={!hash}
>
<Copy size={10} color="#3098DC" />
</Button>
</Tooltip>
<Tooltip title={blockExplorerUrl ? t('viewOnCustomBlockExplorer', [blockExplorerUrl]) : t('viewOnEtherscan')}>
<Button
type="raised"
onClick={this.handleEtherscanClick}
className="transaction-list-item-details__header-button"
disabled={!hash}
>
<img src="/images/arrow-popout.svg" />
</Button>
</Tooltip>
{
showRetry && (
<Tooltip title={blockExplorerUrl ? t('viewOnCustomBlockExplorer', [blockExplorerUrl]) : t('retryTransaction')}>
<Popover title={title} onClose={onClose}>
<div className="transaction-list-item-details">
<div className="transaction-list-item-details__header">
<div>{ t('details') }</div>
<div className="transaction-list-item-details__header-buttons">
{
showSpeedUp && (
<Button
type="raised"
onClick={this.handleRetry}
className="transaction-list-item-details__header-button"
>
<i className="fa fa-sync"></i>
{ t('speedUp') }
</Button>
</Tooltip>
)
}
)
}
{ this.renderCancel() }
<Tooltip title={justCopied ? t('copiedTransactionId') : t('copyTransactionId')}>
<Button
type="raised"
onClick={this.handleCopyTxId}
className="transaction-list-item-details__header-button"
disabled={!hash}
>
<Copy size={10} color="#3098DC" />
</Button>
</Tooltip>
<Tooltip title={blockExplorerUrl ? t('viewOnCustomBlockExplorer', [blockExplorerUrl]) : t('viewOnEtherscan')}>
<Button
type="raised"
onClick={this.handleEtherscanClick}
className="transaction-list-item-details__header-button"
disabled={!hash}
>
<img src="/images/arrow-popout.svg" />
</Button>
</Tooltip>
{
showRetry && (
<Tooltip title={blockExplorerUrl ? t('viewOnCustomBlockExplorer', [blockExplorerUrl]) : t('retryTransaction')}>
<Button
type="raised"
onClick={this.handleRetry}
className="transaction-list-item-details__header-button"
>
<i className="fa fa-sync"></i>
</Button>
</Tooltip>
)
}
</div>
</div>
<div className="transaction-list-item-details__body">
<div className="transaction-list-item-details__sender-to-recipient-container">
<SenderToRecipient
variant={FLAT_VARIANT}
addressOnly
recipientEns={recipientEns}
recipientAddress={recipientAddress}
recipientNickname={recipientNickname}
senderName={senderNickname}
senderAddress={senderAddress}
onRecipientClick={() => {
this.context.metricsEvent({
eventOpts: {
category: 'Navigation',
action: 'Activity Log',
name: 'Copied "To" Address',
},
})
}}
onSenderClick={() => {
this.context.metricsEvent({
eventOpts: {
category: 'Navigation',
action: 'Activity Log',
name: 'Copied "From" Address',
},
})
}}
/>
</div>
<div className="transaction-list-item-details__cards-container">
<TransactionBreakdown
transaction={transaction}
className="transaction-list-item-details__transaction-breakdown"
/>
<TransactionActivityLog
transactionGroup={transactionGroup}
className="transaction-list-item-details__transaction-activity-log"
onCancel={onCancel}
onRetry={onRetry}
isEarliestNonce={isEarliestNonce}
/>
</div>
</div>
</div>
<div className="transaction-list-item-details__body">
<div className="transaction-list-item-details__sender-to-recipient-container">
<SenderToRecipient
variant={FLAT_VARIANT}
addressOnly
recipientEns={recipientEns}
recipientAddress={recipientAddress}
recipientNickname={recipientNickname}
senderName={senderNickname}
senderAddress={senderAddress}
onRecipientClick={() => {
this.context.metricsEvent({
eventOpts: {
category: 'Navigation',
action: 'Activity Log',
name: 'Copied "To" Address',
},
})
}}
onSenderClick={() => {
this.context.metricsEvent({
eventOpts: {
category: 'Navigation',
action: 'Activity Log',
name: 'Copied "From" Address',
},
})
}}
/>
</div>
<div className="transaction-list-item-details__cards-container">
<TransactionBreakdown
transaction={transaction}
className="transaction-list-item-details__transaction-breakdown"
/>
<TransactionActivityLog
transactionGroup={transactionGroup}
className="transaction-list-item-details__transaction-activity-log"
onCancel={onCancel}
onRetry={onRetry}
isEarliestNonce={isEarliestNonce}
/>
</div>
</div>
</div>
</Popover>
)
}
}

View File

@ -2,7 +2,7 @@ import { connect } from 'react-redux'
import TransactionListItemDetails from './transaction-list-item-details.component'
import { checksumAddress } from '../../../helpers/utils/util'
import { tryReverseResolveAddress } from '../../../store/actions'
import { getAddressBook } from '../../../selectors'
import { getAddressBook, getRpcPrefsForCurrentProvider } from '../../../selectors'
const mapStateToProps = (state, ownProps) => {
const { metamask } = state
@ -20,8 +20,10 @@ const mapStateToProps = (state, ownProps) => {
})
return (entry && entry.name) || ''
}
const rpcPrefs = getRpcPrefsForCurrentProvider(state)
return {
rpcPrefs,
recipientEns,
senderNickname: getNickName(senderAddress),
recipientNickname: getNickName(recipientAddress),

View File

@ -1 +1 @@
export { default } from './transaction-list-item.container'
export { default } from './transaction-list-item.component'

View File

@ -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;
}
&--pending {
color: $Grey-500;
}
&--pending &__primary-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;
&--unapproved {
color: $flamingo;
}
}
&__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;
&--failed {
color: $valencia;
}
}
&__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;
&--cancelled {
color: $valencia;
}
}
&__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;
}
}
&--secondary {
text-align: end;
font-size: .75rem;
grid-area: secondary-amount;
align-self: start;
justify-self: end;
&--queued {
color: $Grey-500;
}
}
&__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;
}
}
&__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;
}
}
}

View File

@ -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'
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 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,
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
}
static defaultProps = {
showFiat: true,
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>
)
}
static contextTypes = {
metricsEvent: PropTypes.func,
}
const className = classnames('transaction-list-item', { 'transaction-list-item--pending': isPending })
state = {
showTransactionDetails: false,
}
componentDidMount () {
if (this.props.data) {
this.props.getContractMethodData(this.props.data)
}
}
handleClick = () => {
const {
transaction,
history,
} = this.props
const { id, status } = transaction
const { showTransactionDetails } = this.state
if (status === UNAPPROVED_STATUS) {
const toggleShowDetails = useCallback(() => {
if (isUnapproved) {
history.push(`${CONFIRM_TRANSACTION_ROUTE}/${id}`)
return
}
setShowDetails((prev) => !prev)
}, [isUnapproved, id])
if (!showTransactionDetails) {
this.context.metricsEvent({
eventOpts: {
category: 'Navigation',
action: 'Home',
name: 'Expand Transaction',
},
})
}
this.setState({ showTransactionDetails: !showTransactionDetails })
}
handleCancel = (id) => {
const {
primaryTransaction: { txParams: { gasPrice } } = {},
transaction: { id: initialTransactionId },
showCancelModal,
} = this.props
const cancelId = id || initialTransactionId
showCancelModal(cancelId, gasPrice)
}
/**
* @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
if (name === TOKEN_METHOD_TRANSFER) {
setSelectedToken(to)
}
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
)}
/>
{ 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>
)
}
</div>
</div>
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
}
return !cancelEnabled ? (
<Tooltip title={t('notEnoughGas')}>
<div>
{cancelButton}
</div>
</Tooltip>
) : cancelButton
}, [cancelEnabled, cancelTransaction, hasCancelled])
const speedUpButton = useMemo(() => {
if (!shouldShowSpeedUp || !isPending || isUnapproved) {
return null
}
return (
<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"
/>
)}
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>
</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,
}

View File

@ -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)

View File

@ -1 +1 @@
export { default } from './transaction-list.container'
export { default } from './transaction-list.component'

View File

@ -2,7 +2,6 @@
display: flex;
flex-direction: column;
flex: 1;
margin-top: 8px;
&__completed-transactions {
display: flex;
@ -44,4 +43,8 @@
justify-content: center;
color: $silver;
}
&__view-more {
margin: 16px auto;
max-width: 200px;
}
}

View File

@ -1,79 +1,56 @@
import React, { PureComponent } from 'react'
import React, { useMemo, useEffect, useRef, useState, useCallback } from 'react'
import PropTypes from 'prop-types'
import { useSelector, useDispatch } from 'react-redux'
import {
nonceSortedCompletedTransactionsSelector,
nonceSortedPendingTransactionsSelector,
} from '../../../selectors/transactions'
import {
getFeatureFlags,
} from '../../../selectors/selectors'
import * as actions from '../../../ducks/gas/gas.duck'
import { useI18nContext } from '../../../hooks/useI18nContext'
import TransactionListItem from '../transaction-list-item'
import Button from '../../ui/button'
export default class TransactionList extends PureComponent {
static contextTypes = {
t: PropTypes.func,
}
const PAGE_INCREMENT = 10
static defaultProps = {
pendingTransactions: [],
completedTransactions: [],
}
export default function TransactionList ({ isWideViewport = false } = {}) {
const [limit, setLimit] = useState(PAGE_INCREMENT)
const t = useI18nContext()
static propTypes = {
isWideViewport: PropTypes.bool.isRequired,
pendingTransactions: PropTypes.array,
completedTransactions: PropTypes.array,
selectedToken: PropTypes.object,
assetImages: PropTypes.object,
fetchBasicGasAndTimeEstimates: PropTypes.func,
fetchGasEstimates: PropTypes.func,
transactionTimeFeatureActive: PropTypes.bool,
firstPendingTransactionId: PropTypes.number,
}
const dispatch = useDispatch()
const pendingTransactions = useSelector(nonceSortedPendingTransactionsSelector)
const completedTransactions = useSelector(nonceSortedCompletedTransactionsSelector)
const { transactionTime: transactionTimeFeatureActive } = useSelector(getFeatureFlags)
componentDidMount () {
const {
pendingTransactions,
fetchBasicGasAndTimeEstimates,
fetchGasEstimates,
transactionTimeFeatureActive,
} = this.props
const { fetchGasEstimates, fetchBasicGasAndTimeEstimates } = useMemo(() => ({
fetchGasEstimates: (blockTime) => dispatch(actions.fetchGasEstimates(blockTime)),
fetchBasicGasAndTimeEstimates: () => dispatch(actions.fetchBasicGasAndTimeEstimates()),
}), [dispatch])
if (transactionTimeFeatureActive && pendingTransactions.length) {
// keep track of previous values from state.
// loaded is used here to determine if our effect has ran at least once.
const prevState = useRef({ loaded: false, pendingTransactions, transactionTimeFeatureActive })
useEffect(() => {
const { loaded } = prevState.current
const pendingTransactionAdded = pendingTransactions.length > 0 && prevState.current.pendingTransactions.length === 0
const transactionTimeFeatureWasActivated = !prevState.current.transactionTimeFeatureActive && transactionTimeFeatureActive
if (transactionTimeFeatureActive && pendingTransactions.length > 0 && (loaded === false || transactionTimeFeatureWasActivated || pendingTransactionAdded)) {
fetchBasicGasAndTimeEstimates()
.then(({ blockTime }) => fetchGasEstimates(blockTime))
}
}
prevState.current = { loaded: true, pendingTransactions, transactionTimeFeatureActive }
}, [fetchGasEstimates, fetchBasicGasAndTimeEstimates, transactionTimeFeatureActive, pendingTransactions ])
componentDidUpdate (prevProps) {
const { pendingTransactions: prevPendingTransactions = [] } = prevProps
const {
pendingTransactions = [],
fetchBasicGasAndTimeEstimates,
fetchGasEstimates,
transactionTimeFeatureActive,
} = this.props
const viewMore = useCallback(() => setLimit((prev) => prev + PAGE_INCREMENT), [])
const transactionTimeFeatureWasActivated = !prevProps.transactionTimeFeatureActive && transactionTimeFeatureActive
const pendingTransactionAdded = pendingTransactions.length > 0 && prevPendingTransactions.length === 0
if (transactionTimeFeatureActive && pendingTransactions.length > 0 && (transactionTimeFeatureWasActivated || pendingTransactionAdded)) {
fetchBasicGasAndTimeEstimates()
.then(({ blockTime }) => fetchGasEstimates(blockTime))
}
}
const pendingLength = pendingTransactions.length
shouldShowSpeedUp = (transactionGroup, isEarliestNonce) => {
const { transactions = [], hasRetried } = transactionGroup
const [earliestTransaction = {}] = transactions
const { submittedTime } = earliestTransaction
return Date.now() - submittedTime > 5000 && isEarliestNonce && !hasRetried
}
shouldShowCancel (transactionGroup) {
const { hasCancelled } = transactionGroup
return !hasCancelled
}
renderTransactions () {
const { t } = this.context
const { isWideViewport, pendingTransactions = [], completedTransactions = [] } = this.props
const pendingLength = pendingTransactions.length
return (
return (
<div className="transaction-list">
<div className="transaction-list__transactions">
{
pendingLength > 0 && (
@ -83,7 +60,7 @@ export default class TransactionList extends PureComponent {
</div>
{
pendingTransactions.map((transactionGroup, index) => (
this.renderTransaction(transactionGroup, index, true)
<TransactionListItem isEarliestNonce={index === 0} transactionGroup={transactionGroup} key={`${transactionGroup.nonce}:${index}`} />
))
}
</div>
@ -101,48 +78,26 @@ export default class TransactionList extends PureComponent {
}
{
completedTransactions.length > 0
? completedTransactions.map((transactionGroup, index) => (
this.renderTransaction(transactionGroup, index)
? completedTransactions.slice(0, limit).map((transactionGroup, index) => (
<TransactionListItem transactionGroup={transactionGroup} key={`${transactionGroup.nonce}:${limit + index - 10}`} />
))
: this.renderEmpty()
: (
<div className="transaction-list__empty">
<div className="transaction-list__empty-text">
{ t('noTransactions') }
</div>
</div>
)
}
{(completedTransactions.length - limit + PAGE_INCREMENT) > 0 && (
<Button className="transaction-list__view-more" type="secondary" rounded onClick={viewMore}>View More</Button>
)}
</div>
</div>
)
}
renderTransaction (transactionGroup, index, isPendingTx = false) {
const { selectedToken, assetImages, firstPendingTransactionId } = this.props
return (
<TransactionListItem
transactionGroup={transactionGroup}
key={`${transactionGroup.nonce}:${index}`}
showSpeedUp={isPendingTx && this.shouldShowSpeedUp(transactionGroup, index === 0)}
showCancel={isPendingTx && this.shouldShowCancel(transactionGroup)}
isEarliestNonce={isPendingTx && index === 0}
token={selectedToken}
assetImages={assetImages}
firstPendingTransactionId={firstPendingTransactionId}
/>
)
}
renderEmpty () {
return (
<div className="transaction-list__empty">
<div className="transaction-list__empty-text">
{ this.context.t('noTransactions') }
</div>
</div>
)
}
render () {
return (
<div className="transaction-list">
{ this.renderTransactions() }
</div>
)
}
</div>
)
}
TransactionList.propTypes = {
isWideViewport: PropTypes.bool.isRequired,
}

View File

@ -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

View File

@ -8,6 +8,7 @@ const CLASSNAME_SECONDARY = 'btn-secondary'
const CLASSNAME_CONFIRM = 'btn-primary'
const CLASSNAME_RAISED = 'btn-raised'
const CLASSNAME_LARGE = 'btn--large'
const CLASSNAME_ROUNDED = 'btn--rounded'
const CLASSNAME_FIRST_TIME = 'btn--first-time'
const typeHash = {
@ -24,13 +25,14 @@ const typeHash = {
'first-time': CLASSNAME_FIRST_TIME,
}
const Button = ({ type, submit, large, children, className, ...buttonProps }) => (
const Button = ({ type, submit, large, children, rounded, className, ...buttonProps }) => (
<button
type={submit ? 'submit' : undefined}
className={classnames(
'button',
typeHash[type] || CLASSNAME_DEFAULT,
large && CLASSNAME_LARGE,
rounded && CLASSNAME_ROUNDED,
className
)}
{ ...buttonProps }
@ -43,6 +45,7 @@ Button.propTypes = {
type: PropTypes.string,
submit: PropTypes.bool,
large: PropTypes.bool,
rounded: PropTypes.bool,
className: PropTypes.string,
children: PropTypes.node,
}

View File

@ -8,6 +8,8 @@ $hover-confirm: #0372C3;
$hover-red: #FEB6BF;
$hover-red-primary: #C72837;
$hover-orange: #FFD3B5;
$danger-light-red: #EA7E77;
$warning-light-orange: #F8B588;
%button {
@include h6;
@ -244,3 +246,88 @@ button.primary {
font-family: Roboto;
text-transform: uppercase;
}
.btn--rounded {
border-radius: 100px;
will-change: box-shadow;
transition: box-shadow cubic-bezier(0.6, -0.28, 0.735, 0.045) 200ms;
&:hover {
box-shadow: 0px 0px 8px rgba(0, 0, 0, 0.1);
}
&.btn-secondary {
border: 1px solid $Blue-500;
&--disabled,
&[disabled] {
border-color: $hover-secondary;
color: $hover-secondary;
}
&:active {
border-color: $Blue-600;
}
}
&.btn-default {
border: 1px solid $Grey-500;
&--disabled,
&[disabled] {
border-color: $Grey-100;
color: $hover-default;
}
&:active {
border-color: $Grey-600;
}
}
&.btn-danger {
border: 1px solid $Red-500;
&--disabled,
&[disabled] {
border-color: $Red-100;
color: $danger-light-red;
}
&:active {
border-color: $Red-600;
}
}
&.btn-warning {
border: 1px solid $Orange-500;
&--disabled,
&[disabled] {
border-color: $warning-light-orange;
color: $warning-light-orange;
}
&:active {
border-color: $Orange-600;
}
}
&.btn-primary {
background-color: $Blue-500;
&--disabled,
&[disabled] {
background-color: $hover-secondary;
}
&:active {
background-color: $Blue-600;
}
}
&.btn-danger-primary {
background-color: $Red-500;
&--disabled,
&[disabled] {
background-color: $danger-light-red;
}
&:active {
background-color: $Red-600;
}
}
}

View File

@ -45,7 +45,7 @@
color: $Grey-500;
}
&__rightContent {
&__right-content {
margin: 0 0 0 auto;
text-align: right;
}

View File

@ -30,7 +30,7 @@ export default function ListItem ({ title, subtitle, onClick, subtitleStatus, ch
)}
</div>
{rightContent && (
<div className="list-item__col list-item__rightContent">
<div className="list-item__col list-item__right-content">
{rightContent}
</div>
)}

View File

@ -8,10 +8,29 @@ export const FAILED_STATUS = 'failed'
export const DROPPED_STATUS = 'dropped'
export const CANCELLED_STATUS = 'cancelled'
export const PENDING_STATUS_HASH = {
[UNAPPROVED_STATUS]: true,
[APPROVED_STATUS]: true,
[SUBMITTED_STATUS]: true,
}
export const PRIORITY_STATUS_HASH = {
...PENDING_STATUS_HASH,
[CONFIRMED_STATUS]: true,
}
export const TOKEN_METHOD_TRANSFER = 'transfer'
export const TOKEN_METHOD_APPROVE = 'approve'
export const TOKEN_METHOD_TRANSFER_FROM = 'transferfrom'
export const TOKEN_CATEGORY_HASH = {
[TOKEN_METHOD_APPROVE]: true,
[TOKEN_METHOD_TRANSFER]: true,
[TOKEN_METHOD_TRANSFER_FROM]: true,
}
export const INCOMING_TRANSACTION = 'incoming'
export const SEND_ETHER_ACTION_KEY = 'sentEther'
export const DEPLOY_CONTRACT_ACTION_KEY = 'contractDeployment'
export const APPROVE_ACTION_KEY = 'approve'
@ -23,3 +42,11 @@ export const ENCRYPTION_PUBLIC_KEY_REQUEST_KEY = 'encryptionPublicKeyRequest'
export const CONTRACT_INTERACTION_KEY = 'contractInteraction'
export const CANCEL_ATTEMPT_ACTION_KEY = 'cancelAttempt'
export const DEPOSIT_TRANSACTION_KEY = 'deposit'
// Transaction List Item Categories
// Used for UI distinction between transactions in the history list
export const TRANSACTION_CATEGORY_SEND = 'send'
export const TRANSACTION_CATEGORY_RECEIVE = 'receive'
export const TRANSACTION_CATEGORY_INTERACTION = 'interaction'
export const TRANSACTION_CATEGORY_APPROVAL = 'approval'
export const TRANSACTION_CATEGORY_SIGNATURE_REQUEST = 'signature-request'

View File

@ -8,6 +8,12 @@ export function formatDate (date, format = 'M/d/y \'at\' T') {
return DateTime.fromMillis(date).toFormat(format)
}
export function formatDateWithYearContext (date, formatThisYear = 'MMM d', fallback = 'MMM d, y') {
const dateTime = DateTime.fromMillis(date)
const now = DateTime.local()
return dateTime.toFormat(now.year === dateTime.year ? formatThisYear : fallback)
}
const valueTable = {
wei: '1000000000000000000',
kwei: '1000000000000000',

View File

@ -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()
})
})

View File

@ -3,6 +3,7 @@ import { renderHook } from '@testing-library/react-hooks'
import * as reactRedux from 'react-redux'
import { useCurrencyDisplay } from '../useCurrencyDisplay'
import sinon from 'sinon'
import { getCurrentCurrency, getNativeCurrency, getConversionRate } from '../../selectors'
const tests = [
{
@ -99,11 +100,15 @@ describe('useCurrencyDisplay', function () {
tests.forEach(({ input: { value, ...restProps }, result }) => {
describe(`when input is { value: ${value}, decimals: ${restProps.numberOfDecimals}, denomation: ${restProps.denomination} }`, function () {
const stub = sinon.stub(reactRedux, 'useSelector')
stub.callsFake(() => ({
currentCurrency: 'usd',
nativeCurrency: 'ETH',
conversionRate: 280.45,
}))
stub.callsFake((selector) => {
if (selector === getCurrentCurrency) {
return 'usd'
} else if (selector === getNativeCurrency) {
return 'ETH'
} else if (selector === getConversionRate) {
return 280.45
}
})
const hookReturn = renderHook(() => useCurrencyDisplay(value, restProps))
const [ displayValue, parts ] = hookReturn.result.current
stub.restore()

View File

@ -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()
})
})
})

View File

@ -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)
})
})
})

View File

@ -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()
})
})

View File

@ -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]
}

View File

@ -1,6 +1,7 @@
import { useMemo } from 'react'
import { useSelector } from 'react-redux'
import { formatCurrency, getValueFromWeiHex } from '../helpers/utils/confirm-tx.util'
import { getCurrentCurrency, getConversionRate, getNativeCurrency } from '../selectors'
/**
* Defines the shape of the options parameter for useCurrencyDisplay
@ -31,13 +32,9 @@ import { formatCurrency, getValueFromWeiHex } from '../helpers/utils/confirm-tx.
* @return {[string, CurrencyDisplayParts]}
*/
export function useCurrencyDisplay (inputValue, { displayValue, prefix, numberOfDecimals, denomination, currency, ...opts }) {
const { currentCurrency, nativeCurrency, conversionRate } = useSelector(
({ metamask: { currentCurrency, nativeCurrency, conversionRate } }) => ({
currentCurrency,
nativeCurrency,
conversionRate,
})
)
const currentCurrency = useSelector(getCurrentCurrency)
const nativeCurrency = useSelector(getNativeCurrency)
const conversionRate = useSelector(getConversionRate)
const toCurrency = currency || currentCurrency

View File

@ -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
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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])
}

View File

@ -1,6 +1,6 @@
import { getTokenValue, calcTokenAmount } from '../helpers/utils/token-util'
import { getTokenData } from '../helpers/utils/transactions.util'
import { useMemo } from 'react'
import { useTokenData } from './useTokenData'
/**
* Defines the shape for the Token input parameter for useTokenDisplayValue
@ -15,24 +15,35 @@ import { useMemo } from 'react'
* a displayValue that represents a string representing that token amount as a string. Also
* return a tokenData object for downstream usage and the suffix for the token to use as props
* for other hooks and/or components
* @param {string} transactionData
* @param {Token} token
* @param {string} [transactionData] - Raw data string from token transaction
* @param {Token} [token] - The token associated with this 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 {string} - The computed displayValue of the provided transactionData and token
*/
export function useTokenDisplayValue (transactionData, token) {
if (!transactionData || !token) {
return null
}
const tokenData = useMemo(() => getTokenData(transactionData), [transactionData])
if (!tokenData?.params?.length) {
return null
}
const { decimals } = token
export function useTokenDisplayValue (transactionData, token, isTokenTransaction = true) {
const tokenData = useTokenData(transactionData, isTokenTransaction)
const shouldCalculateTokenValue = Boolean(
// If we are currently processing a token transaction
isTokenTransaction &&
// and raw transaction data string is provided
transactionData &&
// and a token object has been provided
token &&
// and we are able to parse the token details from the raw data
tokenData?.params?.length
)
const displayValue = useMemo(() => {
if (!shouldCalculateTokenValue) {
return null
}
const tokenValue = getTokenValue(tokenData.params)
return calcTokenAmount(tokenValue, decimals).toString()
}, [tokenData, decimals])
return calcTokenAmount(tokenValue, token.decimals).toString()
}, [shouldCalculateTokenValue, tokenData, token])
return displayValue
}

View File

@ -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,
}
}

View File

@ -1,4 +1,4 @@
import { getPreferences, getShouldShowFiat } from '../selectors'
import { getPreferences, getShouldShowFiat, getNativeCurrency } from '../selectors'
import { useSelector } from 'react-redux'
import { PRIMARY, SECONDARY, ETH } from '../helpers/constants/common'
@ -30,7 +30,7 @@ import { PRIMARY, SECONDARY, ETH } from '../helpers/constants/common'
* @return {UserPreferredCurrency}
*/
export function useUserPreferencedCurrency (type, opts = {}) {
const nativeCurrency = useSelector((state) => state.metamask.nativeCurrency)
const nativeCurrency = useSelector(getNativeCurrency)
const {
useNativeCurrencyAsPrimaryCurrency,
} = useSelector(getPreferences)

View File

@ -1,9 +1,9 @@
import { createSelector } from 'reselect'
import {
UNAPPROVED_STATUS,
APPROVED_STATUS,
SUBMITTED_STATUS,
CONFIRMED_STATUS,
PRIORITY_STATUS_HASH,
PENDING_STATUS_HASH,
} from '../helpers/constants/transactions'
import {
TRANSACTION_TYPE_CANCEL,
@ -72,17 +72,6 @@ export const unapprovedMessagesSelector = createSelector(
) || []
)
const pendingStatusHash = {
[UNAPPROVED_STATUS]: true,
[APPROVED_STATUS]: true,
[SUBMITTED_STATUS]: true,
}
const priorityStatusHash = {
...pendingStatusHash,
[CONFIRMED_STATUS]: true,
}
export const transactionSubSelector = createSelector(
unapprovedMessagesSelector,
incomingTxListSelector,
@ -250,7 +239,7 @@ export const nonceSortedTransactionsSelector = createSelector(
const nonceProps = nonceToTransactionsMap[nonce]
insertTransactionByTime(nonceProps.transactions, transaction)
if (status in priorityStatusHash) {
if (status in PRIORITY_STATUS_HASH) {
const { primaryTransaction: { time: primaryTxTime = 0 } = {} } = nonceProps
if (status === CONFIRMED_STATUS || txTime > primaryTxTime) {
@ -302,7 +291,7 @@ export const nonceSortedTransactionsSelector = createSelector(
export const nonceSortedPendingTransactionsSelector = createSelector(
nonceSortedTransactionsSelector,
(transactions = []) => (
transactions.filter(({ primaryTransaction }) => primaryTransaction.status in pendingStatusHash)
transactions.filter(({ primaryTransaction }) => primaryTransaction.status in PENDING_STATUS_HASH)
)
)
@ -316,7 +305,7 @@ export const nonceSortedCompletedTransactionsSelector = createSelector(
nonceSortedTransactionsSelector,
(transactions = []) => (
transactions
.filter(({ primaryTransaction }) => !(primaryTransaction.status in pendingStatusHash))
.filter(({ primaryTransaction }) => !(primaryTransaction.status in PENDING_STATUS_HASH))
.reverse()
)
)