mirror of
https://github.com/kremalicious/metamask-extension.git
synced 2024-11-22 09:57:02 +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:
parent
e06fb2c9f6
commit
706dc02cb4
@ -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:"
|
||||
},
|
||||
|
@ -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",
|
||||
|
499
test/data/transaction-data.json
Normal file
499
test/data/transaction-data.json
Normal 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
|
||||
}
|
||||
]
|
@ -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)
|
||||
})
|
||||
})
|
||||
|
@ -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()))
|
||||
})
|
||||
|
@ -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)
|
||||
})
|
||||
})
|
||||
|
@ -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/))
|
||||
})
|
||||
})
|
||||
|
@ -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()))
|
||||
})
|
||||
|
@ -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')
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
})
|
||||
|
@ -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>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -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),
|
||||
|
@ -1 +1 @@
|
||||
export { default } from './transaction-list-item.container'
|
||||
export { default } from './transaction-list-item.component'
|
||||
|
@ -1,149 +1,50 @@
|
||||
.transaction-list-item {
|
||||
box-sizing: border-box;
|
||||
min-height: 74px;
|
||||
border-bottom: 1px solid $Grey-100;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
flex-direction: column;
|
||||
background: $white;
|
||||
cursor: pointer;
|
||||
|
||||
&__grid {
|
||||
cursor: pointer;
|
||||
width: 100%;
|
||||
padding: 16px 20px;
|
||||
display: grid;
|
||||
grid-template-columns: 45px 1fr 1fr 1fr 1fr;
|
||||
grid-template-areas:
|
||||
"identicon action status estimated-time primary-amount"
|
||||
"identicon nonce status estimated-time secondary-amount";
|
||||
grid-template-rows: 24px;
|
||||
|
||||
@media screen and (max-width: $break-small) {
|
||||
padding: .5rem 1rem;
|
||||
grid-template-columns: 45px 5fr 3fr;
|
||||
grid-template-areas:
|
||||
"nonce nonce nonce nonce"
|
||||
"identicon action estimated-time primary-amount"
|
||||
"identicon status estimated-time secondary-amount";
|
||||
grid-template-rows: auto 24px;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background: rgba($alto, .2);
|
||||
}
|
||||
&:hover {
|
||||
background-color: $Grey-000;
|
||||
}
|
||||
|
||||
&__identicon {
|
||||
grid-area: identicon;
|
||||
grid-row: 1 / span 2;
|
||||
align-self: center;
|
||||
|
||||
@media screen and (max-width: $break-small) {
|
||||
grid-row: 2 / span 2;
|
||||
}
|
||||
&__primary-currency {
|
||||
color: $Black-100;
|
||||
}
|
||||
|
||||
&__action {
|
||||
text-transform: capitalize;
|
||||
padding: 0 0 4px 0;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
grid-area: action;
|
||||
color: $Grey-800;
|
||||
line-height: 20px;
|
||||
&__secondary-currency {
|
||||
color: $Grey-500;
|
||||
}
|
||||
|
||||
&--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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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,
|
||||
}
|
||||
|
@ -1,113 +0,0 @@
|
||||
import { connect } from 'react-redux'
|
||||
import { withRouter } from 'react-router-dom'
|
||||
import { compose } from 'redux'
|
||||
import TransactionListItem from './transaction-list-item.component'
|
||||
import { setSelectedToken, showModal, showSidebar, getContractMethodData } from '../../../store/actions'
|
||||
import { hexToDecimal } from '../../../helpers/utils/conversions.util'
|
||||
import { getTokenData } from '../../../helpers/utils/transactions.util'
|
||||
import { getHexGasTotal, increaseLastGasPrice } from '../../../helpers/utils/confirm-tx.util'
|
||||
import { formatDate } from '../../../helpers/utils/util'
|
||||
import {
|
||||
fetchGasEstimates,
|
||||
fetchBasicGasAndTimeEstimates,
|
||||
setCustomGasPriceForRetry,
|
||||
setCustomGasLimit,
|
||||
} from '../../../ducks/gas/gas.duck'
|
||||
import {
|
||||
getIsMainnet,
|
||||
getPreferences,
|
||||
getSelectedAddress,
|
||||
conversionRateSelector,
|
||||
getKnownMethodData,
|
||||
getFeatureFlags,
|
||||
} from '../../../selectors'
|
||||
import { isBalanceSufficient } from '../../../pages/send/send.utils'
|
||||
|
||||
const mapStateToProps = (state, ownProps) => {
|
||||
const { metamask: { accounts, provider, frequentRpcListDetail } } = state
|
||||
const { showFiatInTestnets } = getPreferences(state)
|
||||
const isMainnet = getIsMainnet(state)
|
||||
const { transactionGroup: { primaryTransaction } = {} } = ownProps
|
||||
const { txParams: { gas: gasLimit, gasPrice, data } = {}, transactionCategory } = primaryTransaction
|
||||
const selectedAddress = getSelectedAddress(state)
|
||||
const selectedAccountBalance = accounts[selectedAddress].balance
|
||||
const isDeposit = transactionCategory === 'incoming'
|
||||
const selectRpcInfo = frequentRpcListDetail.find((rpcInfo) => rpcInfo.rpcUrl === provider.rpcTarget)
|
||||
const { rpcPrefs } = selectRpcInfo || {}
|
||||
|
||||
const hasEnoughCancelGas = primaryTransaction.txParams && isBalanceSufficient({
|
||||
amount: '0x0',
|
||||
gasTotal: getHexGasTotal({
|
||||
gasPrice: increaseLastGasPrice(gasPrice),
|
||||
gasLimit,
|
||||
}),
|
||||
balance: selectedAccountBalance,
|
||||
conversionRate: conversionRateSelector(state),
|
||||
})
|
||||
|
||||
const transactionTimeFeatureActive = getFeatureFlags(state).transactionTime
|
||||
|
||||
return {
|
||||
methodData: getKnownMethodData(state, data) || {},
|
||||
showFiat: (isMainnet || !!showFiatInTestnets),
|
||||
selectedAccountBalance,
|
||||
hasEnoughCancelGas,
|
||||
rpcPrefs,
|
||||
isDeposit,
|
||||
transactionTimeFeatureActive,
|
||||
}
|
||||
}
|
||||
|
||||
const mapDispatchToProps = (dispatch) => {
|
||||
return {
|
||||
fetchBasicGasAndTimeEstimates: () => dispatch(fetchBasicGasAndTimeEstimates()),
|
||||
fetchGasEstimates: (blockTime) => dispatch(fetchGasEstimates(blockTime)),
|
||||
setSelectedToken: (tokenAddress) => dispatch(setSelectedToken(tokenAddress)),
|
||||
getContractMethodData: (methodData) => dispatch(getContractMethodData(methodData)),
|
||||
retryTransaction: (transaction, gasPrice) => {
|
||||
dispatch(setCustomGasPriceForRetry(gasPrice || transaction.txParams.gasPrice))
|
||||
dispatch(setCustomGasLimit(transaction.txParams.gas))
|
||||
dispatch(showSidebar({
|
||||
transitionName: 'sidebar-left',
|
||||
type: 'customize-gas',
|
||||
props: { transaction },
|
||||
}))
|
||||
},
|
||||
showCancelModal: (transactionId, originalGasPrice) => {
|
||||
return dispatch(showModal({ name: 'CANCEL_TRANSACTION', transactionId, originalGasPrice }))
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
const mergeProps = (stateProps, dispatchProps, ownProps) => {
|
||||
const { transactionGroup: { primaryTransaction, initialTransaction } = {} } = ownProps
|
||||
const { isDeposit } = stateProps
|
||||
const { retryTransaction, ...restDispatchProps } = dispatchProps
|
||||
const { txParams: { nonce, data } = {}, time = 0 } = initialTransaction
|
||||
const { txParams: { value } = {} } = primaryTransaction
|
||||
|
||||
const tokenData = data && getTokenData(data)
|
||||
const nonceAndDate = nonce && !isDeposit ? `#${hexToDecimal(nonce)} - ${formatDate(time)}` : formatDate(time)
|
||||
|
||||
return {
|
||||
...stateProps,
|
||||
...restDispatchProps,
|
||||
...ownProps,
|
||||
value,
|
||||
nonceAndDate,
|
||||
tokenData,
|
||||
transaction: initialTransaction,
|
||||
primaryTransaction,
|
||||
retryTransaction: (transactionId, gasPrice) => {
|
||||
const { transactionGroup: { transactions = [] } } = ownProps
|
||||
const transaction = transactions.find((tx) => tx.id === transactionId) || {}
|
||||
const increasedGasPrice = increaseLastGasPrice(gasPrice)
|
||||
retryTransaction(transaction, increasedGasPrice)
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
export default compose(
|
||||
withRouter,
|
||||
connect(mapStateToProps, mapDispatchToProps, mergeProps),
|
||||
)(TransactionListItem)
|
@ -1 +1 @@
|
||||
export { default } from './transaction-list.container'
|
||||
export { default } from './transaction-list.component'
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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,
|
||||
}
|
||||
|
@ -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
|
@ -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,
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -45,7 +45,7 @@
|
||||
color: $Grey-500;
|
||||
}
|
||||
|
||||
&__rightContent {
|
||||
&__right-content {
|
||||
margin: 0 0 0 auto;
|
||||
text-align: right;
|
||||
}
|
||||
|
@ -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>
|
||||
)}
|
||||
|
@ -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'
|
||||
|
@ -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',
|
||||
|
103
ui/app/hooks/tests/useCancelTransaction.test.js
Normal file
103
ui/app/hooks/tests/useCancelTransaction.test.js
Normal 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()
|
||||
})
|
||||
})
|
@ -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()
|
||||
|
66
ui/app/hooks/tests/useRetryTransaction.test.js
Normal file
66
ui/app/hooks/tests/useRetryTransaction.test.js
Normal 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()
|
||||
})
|
||||
})
|
||||
})
|
76
ui/app/hooks/tests/useTokenData.test.js
Normal file
76
ui/app/hooks/tests/useTokenData.test.js
Normal 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)
|
||||
})
|
||||
})
|
||||
})
|
146
ui/app/hooks/tests/useTransactionDisplayData.test.js
Normal file
146
ui/app/hooks/tests/useTransactionDisplayData.test.js
Normal 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()
|
||||
})
|
||||
})
|
50
ui/app/hooks/useCancelTransaction.js
Normal file
50
ui/app/hooks/useCancelTransaction.js
Normal 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]
|
||||
}
|
@ -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
|
||||
|
||||
|
30
ui/app/hooks/useMethodData.js
Normal file
30
ui/app/hooks/useMethodData.js
Normal 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
|
||||
}
|
62
ui/app/hooks/useRetryTransaction.js
Normal file
62
ui/app/hooks/useRetryTransaction.js
Normal 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
|
||||
}
|
46
ui/app/hooks/useShouldShowSpeedUp.js
Normal file
46
ui/app/hooks/useShouldShowSpeedUp.js
Normal 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
|
||||
}
|
23
ui/app/hooks/useTokenData.js
Normal file
23
ui/app/hooks/useTokenData.js
Normal 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])
|
||||
}
|
@ -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
|
||||
}
|
||||
|
153
ui/app/hooks/useTransactionDisplayData.js
Normal file
153
ui/app/hooks/useTransactionDisplayData.js
Normal 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,
|
||||
}
|
||||
}
|
@ -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)
|
||||
|
@ -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()
|
||||
)
|
||||
)
|
||||
|
Loading…
Reference in New Issue
Block a user