mirror of
https://github.com/kremalicious/metamask-extension.git
synced 2024-11-22 09:57:02 +01:00
Merge branch 'develop' of github.com:MetaMask/metamask-extension into initial-trezor-support
This commit is contained in:
commit
07d8bfaec5
@ -2,6 +2,8 @@
|
||||
|
||||
## Current Master
|
||||
|
||||
- Remove rejected transactions from transaction history
|
||||
|
||||
## 4.8.0 Thur Jun 14 2018
|
||||
|
||||
- [#4513](https://github.com/MetaMask/metamask-extension/pull/4513): Attempting to import an empty private key will now show a clear error.
|
||||
|
@ -9,7 +9,7 @@ If you're a user seeking support, [here is our support site](https://metamask.he
|
||||
|
||||
[Mission Statement](./MISSION.md)
|
||||
|
||||
[Internal documentation](./docs/jsdocs)
|
||||
[Internal documentation](./docs#documentation)
|
||||
|
||||
## Developing Compatible Dapps
|
||||
|
||||
|
@ -43,6 +43,9 @@
|
||||
"message": "MetaMask",
|
||||
"description": "The name of the application"
|
||||
},
|
||||
"approve": {
|
||||
"message": "Approve"
|
||||
},
|
||||
"approved": {
|
||||
"message": "Approved"
|
||||
},
|
||||
@ -95,6 +98,9 @@
|
||||
"buyCoinbaseExplainer": {
|
||||
"message": "Coinbase is the world’s most popular way to buy and sell bitcoin, ethereum, and litecoin."
|
||||
},
|
||||
"bytes": {
|
||||
"message": "Bytes"
|
||||
},
|
||||
"ok": {
|
||||
"message": "Ok"
|
||||
},
|
||||
@ -176,6 +182,9 @@
|
||||
"copyContractAddress": {
|
||||
"message": "Copy Contract Address"
|
||||
},
|
||||
"copyAddress": {
|
||||
"message": "Copy address to clipboard"
|
||||
},
|
||||
"copyToClipboard": {
|
||||
"message": "Copy to clipboard"
|
||||
},
|
||||
@ -307,6 +316,9 @@
|
||||
"enterPasswordContinue": {
|
||||
"message": "Enter password to continue"
|
||||
},
|
||||
"parameters": {
|
||||
"message": "Parameters"
|
||||
},
|
||||
"passwordNotLongEnough": {
|
||||
"message": "Password not long enough"
|
||||
},
|
||||
@ -351,6 +363,9 @@
|
||||
"fromShapeShift": {
|
||||
"message": "From ShapeShift"
|
||||
},
|
||||
"functionType": {
|
||||
"message": "Function Type"
|
||||
},
|
||||
"gas": {
|
||||
"message": "Gas",
|
||||
"description": "Short indication of gas cost"
|
||||
@ -406,6 +421,9 @@
|
||||
"hereList": {
|
||||
"message": "Here's a list!!!!"
|
||||
},
|
||||
"hexData": {
|
||||
"message": "Hex Data"
|
||||
},
|
||||
"hide": {
|
||||
"message": "Hide"
|
||||
},
|
||||
@ -618,6 +636,9 @@
|
||||
"message": "or",
|
||||
"description": "choice between creating or importing a new account"
|
||||
},
|
||||
"origin": {
|
||||
"message": "Origin"
|
||||
},
|
||||
"password": {
|
||||
"message": "Password"
|
||||
},
|
||||
@ -962,6 +983,9 @@
|
||||
"transactionNumber": {
|
||||
"message": "Transaction Number"
|
||||
},
|
||||
"transfer": {
|
||||
"message": "Transfer"
|
||||
},
|
||||
"transfers": {
|
||||
"message": "Transfers"
|
||||
},
|
||||
|
14
app/images/alert-red.svg
Normal file
14
app/images/alert-red.svg
Normal file
@ -0,0 +1,14 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg width="16px" height="16px" viewBox="0 0 16 16" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<!-- Generator: Sketch 50.2 (55047) - http://www.bohemiancoding.com/sketch -->
|
||||
<title>Artboard Copy</title>
|
||||
<desc>Created with Sketch.</desc>
|
||||
<defs></defs>
|
||||
<g id="Artboard-Copy" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||
<g id="Group-48">
|
||||
<circle id="Oval" fill="#D0021B" cx="8" cy="8" r="8"></circle>
|
||||
<rect id="Rectangle-41" fill="#FFFFFF" x="7" y="3" width="2" height="7" rx="1"></rect>
|
||||
<rect id="Rectangle-41" fill="#FFFFFF" x="7" y="11" width="2" height="2" rx="1"></rect>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 774 B |
19
app/images/alert.svg
Normal file
19
app/images/alert.svg
Normal file
@ -0,0 +1,19 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg width="29px" height="29px" viewBox="0 0 29 29" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<!-- Generator: sketchtool 50.2 (55047) - http://www.bohemiancoding.com/sketch -->
|
||||
<title>7414FFD8-B28A-4593-9D7E-19E73D687B50</title>
|
||||
<desc>Created with sketchtool.</desc>
|
||||
<defs></defs>
|
||||
<g id="Action-Screens" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||
<g id="Approve---insufficient-amount" transform="translate(-69.000000, -166.000000)">
|
||||
<g id="Group-7" transform="translate(53.000000, 51.000000)">
|
||||
<g id="Group-34" transform="translate(0.000000, 91.000000)">
|
||||
<g id="alert" transform="translate(16.000000, 24.000000)">
|
||||
<circle id="Oval" fill="#605A1C" cx="14.5" cy="14.5" r="14.5"></circle>
|
||||
<path d="M16,16.8282967 L14,16.8282967 L14,7 L16,7 L16,16.8282967 Z M16,21 L14,21 L14,19 L16,19 L16,21 Z" id="!" fill="#FFFCDB"></path>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 1.1 KiB |
18
app/images/caret-left.svg
Normal file
18
app/images/caret-left.svg
Normal file
@ -0,0 +1,18 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg width="9px" height="15px" viewBox="0 0 9 15" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<!-- Generator: sketchtool 50.2 (55047) - http://www.bohemiancoding.com/sketch -->
|
||||
<title>8439120D-5704-4273-B416-FEE134322584</title>
|
||||
<desc>Created with sketchtool.</desc>
|
||||
<defs></defs>
|
||||
<g id="Action-Screens" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||
<g id="Approve---insufficient-amount" transform="translate(-75.000000, -69.000000)" stroke="#3099F2" stroke-width="2">
|
||||
<g id="Group-7" transform="translate(53.000000, 51.000000)">
|
||||
<g id="cancel" transform="translate(24.000000, 14.000000)">
|
||||
<g id="Group">
|
||||
<polyline id="Path-8" points="6.1263881 18.0633906 0 11.6306831 6.31493631 5"></polyline>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 992 B |
@ -4,6 +4,7 @@ const KOVAN = 'kovan'
|
||||
const MAINNET = 'mainnet'
|
||||
const LOCALHOST = 'localhost'
|
||||
|
||||
const MAINNET_CODE = 1
|
||||
const ROPSTEN_CODE = 3
|
||||
const RINKEYBY_CODE = 4
|
||||
const KOVAN_CODE = 42
|
||||
@ -13,13 +14,13 @@ const RINKEBY_DISPLAY_NAME = 'Rinkeby'
|
||||
const KOVAN_DISPLAY_NAME = 'Kovan'
|
||||
const MAINNET_DISPLAY_NAME = 'Main Ethereum Network'
|
||||
|
||||
|
||||
module.exports = {
|
||||
ROPSTEN,
|
||||
RINKEBY,
|
||||
KOVAN,
|
||||
MAINNET,
|
||||
LOCALHOST,
|
||||
MAINNET_CODE,
|
||||
ROPSTEN_CODE,
|
||||
RINKEYBY_CODE,
|
||||
KOVAN_CODE,
|
||||
|
@ -288,6 +288,7 @@ class TransactionStateManager extends EventEmitter {
|
||||
*/
|
||||
setTxStatusRejected (txId) {
|
||||
this._setTxStatus(txId, 'rejected')
|
||||
this._removeTx(txId)
|
||||
}
|
||||
|
||||
/**
|
||||
@ -422,6 +423,11 @@ class TransactionStateManager extends EventEmitter {
|
||||
_saveTxList (transactions) {
|
||||
this.store.updateState({ transactions })
|
||||
}
|
||||
|
||||
_removeTx (txId) {
|
||||
const transactionList = this.getFullTxList()
|
||||
this._saveTxList(transactionList.filter((txMeta) => txMeta.id !== txId))
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = TransactionStateManager
|
||||
|
35
app/scripts/migrations/027.js
Normal file
35
app/scripts/migrations/027.js
Normal file
@ -0,0 +1,35 @@
|
||||
// next version number
|
||||
const version = 27
|
||||
|
||||
/*
|
||||
|
||||
normalizes txParams on unconfirmed txs
|
||||
|
||||
*/
|
||||
const clone = require('clone')
|
||||
|
||||
module.exports = {
|
||||
version,
|
||||
|
||||
migrate: async function (originalVersionedData) {
|
||||
const versionedData = clone(originalVersionedData)
|
||||
versionedData.meta.version = version
|
||||
const state = versionedData.data
|
||||
const newState = transformState(state)
|
||||
versionedData.data = newState
|
||||
return versionedData
|
||||
},
|
||||
}
|
||||
|
||||
function transformState (state) {
|
||||
const newState = state
|
||||
|
||||
if (newState.TransactionController) {
|
||||
if (newState.TransactionController.transactions) {
|
||||
const transactions = newState.TransactionController.transactions
|
||||
newState.TransactionController.transactions = transactions.filter((txMeta) => txMeta.status !== 'rejected')
|
||||
}
|
||||
}
|
||||
|
||||
return newState
|
||||
}
|
@ -10,7 +10,7 @@ module.exports = {
|
||||
signPersonalMessage: (msgData, cb) => {
|
||||
const stateUpdate = {
|
||||
unapprovedPersonalMsgs: {},
|
||||
unapprovedPersonalMsgsCount: 0,
|
||||
unapprovedPersonalMsgCount: 0,
|
||||
}
|
||||
return cb(null, stateUpdate)
|
||||
},
|
||||
|
@ -156,5 +156,23 @@
|
||||
"fromDropdownOpen": false,
|
||||
"toDropdownOpen": false,
|
||||
"errors": {}
|
||||
},
|
||||
"confirmTransaction": {
|
||||
"txData": {},
|
||||
"tokenData": {},
|
||||
"methodData": {},
|
||||
"tokenProps": {
|
||||
"tokenDecimals": "",
|
||||
"tokenSymbol": ""
|
||||
},
|
||||
"fiatTransactionAmount": "",
|
||||
"fiatTransactionFee": "",
|
||||
"fiatTransactionTotal": "",
|
||||
"ethTransactionAmount": "",
|
||||
"ethTransactionFee": "",
|
||||
"ethTransactionTotal": "",
|
||||
"hexGasTotal": "",
|
||||
"nonce": "",
|
||||
"fetchingMethodData": false
|
||||
}
|
||||
}
|
||||
|
@ -73,7 +73,7 @@
|
||||
"from": "0x0d0c7188d9c72b019a5da9bca0d127680c22e658"
|
||||
},
|
||||
"status": "unapproved",
|
||||
"time": 1537889069339,
|
||||
"time": 1537889070000,
|
||||
"type": "eth_sign"
|
||||
}
|
||||
},
|
||||
@ -86,11 +86,11 @@
|
||||
"from": "0x0d0c7188d9c72b019a5da9bca0d127680c22e659"
|
||||
},
|
||||
"status": "unapproved",
|
||||
"time": 1517889069339,
|
||||
"time": 1537889065000,
|
||||
"type": "personal_sign"
|
||||
}
|
||||
},
|
||||
"unapprovedPersonalMsgCount": 0,
|
||||
"unapprovedPersonalMsgCount": 1 ,
|
||||
"unapprovedTypedMessages": {
|
||||
"8997167822566869": {
|
||||
"id": 8997167822566869,
|
||||
@ -102,7 +102,7 @@
|
||||
"from": "0x0d0c7188d9c72b019a5da9bca0d127680c22e659"
|
||||
},
|
||||
"status": "unapproved",
|
||||
"time": 1617889069339,
|
||||
"time": 1537889060000,
|
||||
"type": "eth_signTypedData"
|
||||
}
|
||||
},
|
||||
@ -172,5 +172,32 @@
|
||||
"scrollToBottom": false,
|
||||
"forgottenPassword": null
|
||||
},
|
||||
"identities": {}
|
||||
"identities": {},
|
||||
"confirmTransaction": {
|
||||
"txData": {
|
||||
"id": 8927167822566864,
|
||||
"msgParams": {
|
||||
"data": "0x879a053d4800c6354e76c7985a865d2922c82fb5b3f4577b2fe08b998954f2e0",
|
||||
"from": "0x0d0c7188d9c72b019a5da9bca0d127680c22e658"
|
||||
},
|
||||
"status": "unapproved",
|
||||
"time": 1537889069339,
|
||||
"type": "eth_sign"
|
||||
},
|
||||
"tokenData": {},
|
||||
"methodData": {},
|
||||
"tokenProps": {
|
||||
"tokenDecimals": "",
|
||||
"tokenSymbol": ""
|
||||
},
|
||||
"fiatTransactionAmount": "",
|
||||
"fiatTransactionFee": "",
|
||||
"fiatTransactionTotal": "",
|
||||
"ethTransactionAmount": "",
|
||||
"ethTransactionFee": "",
|
||||
"ethTransactionTotal": "",
|
||||
"hexGasTotal": "",
|
||||
"nonce": "",
|
||||
"fetchingMethodData": false
|
||||
}
|
||||
}
|
||||
|
@ -130,5 +130,23 @@
|
||||
"scrollToBottom": false,
|
||||
"forgottenPassword": null
|
||||
},
|
||||
"identities": {}
|
||||
"identities": {},
|
||||
"confirmTransaction": {
|
||||
"txData": {},
|
||||
"tokenData": {},
|
||||
"methodData": {},
|
||||
"tokenProps": {
|
||||
"tokenDecimals": "",
|
||||
"tokenSymbol": ""
|
||||
},
|
||||
"fiatTransactionAmount": "",
|
||||
"fiatTransactionFee": "",
|
||||
"fiatTransactionTotal": "",
|
||||
"ethTransactionAmount": "",
|
||||
"ethTransactionFee": "",
|
||||
"ethTransactionTotal": "",
|
||||
"hexGasTotal": "",
|
||||
"nonce": "",
|
||||
"fetchingMethodData": false
|
||||
}
|
||||
}
|
||||
|
@ -58,5 +58,23 @@
|
||||
}
|
||||
},
|
||||
"identities": {},
|
||||
"computedBalances": {}
|
||||
"computedBalances": {},
|
||||
"confirmTransaction": {
|
||||
"txData": {},
|
||||
"tokenData": {},
|
||||
"methodData": {},
|
||||
"tokenProps": {
|
||||
"tokenDecimals": "",
|
||||
"tokenSymbol": ""
|
||||
},
|
||||
"fiatTransactionAmount": "",
|
||||
"fiatTransactionFee": "",
|
||||
"fiatTransactionTotal": "",
|
||||
"ethTransactionAmount": "",
|
||||
"ethTransactionFee": "",
|
||||
"ethTransactionTotal": "",
|
||||
"hexGasTotal": "",
|
||||
"nonce": "",
|
||||
"fetchingMethodData": false
|
||||
}
|
||||
}
|
||||
|
@ -156,5 +156,23 @@
|
||||
"fromDropdownOpen": false,
|
||||
"toDropdownOpen": false,
|
||||
"errors": {}
|
||||
},
|
||||
"confirmTransaction": {
|
||||
"txData": {},
|
||||
"tokenData": {},
|
||||
"methodData": {},
|
||||
"tokenProps": {
|
||||
"tokenDecimals": "",
|
||||
"tokenSymbol": ""
|
||||
},
|
||||
"fiatTransactionAmount": "",
|
||||
"fiatTransactionFee": "",
|
||||
"fiatTransactionTotal": "",
|
||||
"ethTransactionAmount": "",
|
||||
"ethTransactionFee": "",
|
||||
"ethTransactionTotal": "",
|
||||
"hexGasTotal": "",
|
||||
"nonce": "",
|
||||
"fetchingMethodData": false
|
||||
}
|
||||
}
|
||||
|
@ -135,5 +135,23 @@
|
||||
"fromDropdownOpen": false,
|
||||
"toDropdownOpen": false,
|
||||
"errors": {}
|
||||
},
|
||||
"confirmTransaction": {
|
||||
"txData": {},
|
||||
"tokenData": {},
|
||||
"methodData": {},
|
||||
"tokenProps": {
|
||||
"tokenDecimals": "",
|
||||
"tokenSymbol": ""
|
||||
},
|
||||
"fiatTransactionAmount": "",
|
||||
"fiatTransactionFee": "",
|
||||
"fiatTransactionTotal": "",
|
||||
"ethTransactionAmount": "",
|
||||
"ethTransactionFee": "",
|
||||
"ethTransactionTotal": "",
|
||||
"hexGasTotal": "",
|
||||
"nonce": "",
|
||||
"fetchingMethodData": false
|
||||
}
|
||||
}
|
||||
|
116
package-lock.json
generated
116
package-lock.json
generated
@ -8529,6 +8529,108 @@
|
||||
"xhr-request-promise": "^0.1.2"
|
||||
}
|
||||
},
|
||||
"eth-method-registry": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/eth-method-registry/-/eth-method-registry-1.0.0.tgz",
|
||||
"integrity": "sha1-8Ij3Wdad6f3BK3EEm83GiKMoOLY=",
|
||||
"requires": {
|
||||
"ethjs": "^0.3.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"bn.js": {
|
||||
"version": "4.11.6",
|
||||
"resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.11.6.tgz",
|
||||
"integrity": "sha1-UzRK2xRhehP26N0s4okF0cC6MhU="
|
||||
},
|
||||
"ethjs": {
|
||||
"version": "0.3.9",
|
||||
"resolved": "https://registry.npmjs.org/ethjs/-/ethjs-0.3.9.tgz",
|
||||
"integrity": "sha512-gOQzA3tDUjoLpNONSOALJ/rUFtHi5tXl2mholHasF1cvXhoddqi06yU4OJFJu9AGd6n9v9ywzHlYeIKg1t1hdw==",
|
||||
"requires": {
|
||||
"bn.js": "4.11.6",
|
||||
"ethjs-abi": "0.2.1",
|
||||
"ethjs-contract": "0.2.2",
|
||||
"ethjs-filter": "0.1.8",
|
||||
"ethjs-provider-http": "0.1.6",
|
||||
"ethjs-query": "0.3.7",
|
||||
"ethjs-unit": "0.1.6",
|
||||
"ethjs-util": "0.1.3",
|
||||
"js-sha3": "0.5.5",
|
||||
"number-to-bn": "1.7.0"
|
||||
}
|
||||
},
|
||||
"ethjs-abi": {
|
||||
"version": "0.2.1",
|
||||
"resolved": "https://registry.npmjs.org/ethjs-abi/-/ethjs-abi-0.2.1.tgz",
|
||||
"integrity": "sha1-4KepOn6BFjqUR3utVu3lJKtt5TM=",
|
||||
"requires": {
|
||||
"bn.js": "4.11.6",
|
||||
"js-sha3": "0.5.5",
|
||||
"number-to-bn": "1.7.0"
|
||||
}
|
||||
},
|
||||
"ethjs-contract": {
|
||||
"version": "0.2.2",
|
||||
"resolved": "https://registry.npmjs.org/ethjs-contract/-/ethjs-contract-0.2.2.tgz",
|
||||
"integrity": "sha512-xxPqEjsULQ/QNWuvX6Ako0PGs5RxALA8N/H3+boLvnaXDFZVGpD7H63H1gBCRTZyYqCldPpVlVHuw/rD45vazw==",
|
||||
"requires": {
|
||||
"ethjs-abi": "0.2.0",
|
||||
"ethjs-filter": "0.1.8",
|
||||
"ethjs-util": "0.1.3",
|
||||
"js-sha3": "0.5.5"
|
||||
},
|
||||
"dependencies": {
|
||||
"ethjs-abi": {
|
||||
"version": "0.2.0",
|
||||
"resolved": "https://registry.npmjs.org/ethjs-abi/-/ethjs-abi-0.2.0.tgz",
|
||||
"integrity": "sha1-0+LCIQEVIPxJm3FoIDbBT8wvWyU=",
|
||||
"requires": {
|
||||
"bn.js": "4.11.6",
|
||||
"js-sha3": "0.5.5",
|
||||
"number-to-bn": "1.7.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"ethjs-filter": {
|
||||
"version": "0.1.8",
|
||||
"resolved": "https://registry.npmjs.org/ethjs-filter/-/ethjs-filter-0.1.8.tgz",
|
||||
"integrity": "sha512-qTDPskDL2UadHwjvM8A+WG9HwM4/FoSY3p3rMJORkHltYcAuiQZd2otzOYKcL5w2Q3sbAkW/E3yt/FPFL/AVXA=="
|
||||
},
|
||||
"ethjs-query": {
|
||||
"version": "0.3.7",
|
||||
"resolved": "https://registry.npmjs.org/ethjs-query/-/ethjs-query-0.3.7.tgz",
|
||||
"integrity": "sha512-TZnKUwfkWjy0SowFdPLtmsytCorHi0i4vvkQn7Jg8rZt33cRzKhuzOwKr/G3vdigCc+ePXOhUGMcJSAPlOG44A==",
|
||||
"requires": {
|
||||
"ethjs-format": "0.2.7",
|
||||
"ethjs-rpc": "0.2.0",
|
||||
"promise-to-callback": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"ethjs-rpc": {
|
||||
"version": "0.2.0",
|
||||
"resolved": "https://registry.npmjs.org/ethjs-rpc/-/ethjs-rpc-0.2.0.tgz",
|
||||
"integrity": "sha512-RINulkNZTKnj4R/cjYYtYMnFFaBcVALzbtEJEONrrka8IeoarNB9Jbzn+2rT00Cv8y/CxAI+GgY1d0/i2iQeOg==",
|
||||
"requires": {
|
||||
"promise-to-callback": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"ethjs-util": {
|
||||
"version": "0.1.3",
|
||||
"resolved": "https://registry.npmjs.org/ethjs-util/-/ethjs-util-0.1.3.tgz",
|
||||
"integrity": "sha1-39XqSkANxeQhqInK9H4IGtp4u1U=",
|
||||
"requires": {
|
||||
"is-hex-prefixed": "1.0.0",
|
||||
"strip-hex-prefix": "1.0.0"
|
||||
}
|
||||
},
|
||||
"js-sha3": {
|
||||
"version": "0.5.5",
|
||||
"resolved": "https://registry.npmjs.org/js-sha3/-/js-sha3-0.5.5.tgz",
|
||||
"integrity": "sha1-uvDA6MVK1ZA0R9+Wreekobynmko="
|
||||
}
|
||||
}
|
||||
},
|
||||
"eth-phishing-detect": {
|
||||
"version": "1.1.12",
|
||||
"resolved": "https://registry.npmjs.org/eth-phishing-detect/-/eth-phishing-detect-1.1.12.tgz",
|
||||
@ -26341,6 +26443,15 @@
|
||||
"deep-diff": "^0.3.5"
|
||||
}
|
||||
},
|
||||
"redux-mock-store": {
|
||||
"version": "1.5.3",
|
||||
"resolved": "https://registry.npmjs.org/redux-mock-store/-/redux-mock-store-1.5.3.tgz",
|
||||
"integrity": "sha512-ryhkkb/4D4CUGpAV2ln1GOY/uh51aczjcRz9k2L2bPx/Xja3c5pSGJJPyR25GNVRXtKIExScdAgFdiXp68GmJA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"lodash.isplainobject": "^4.0.6"
|
||||
}
|
||||
},
|
||||
"redux-test-utils": {
|
||||
"version": "0.2.2",
|
||||
"resolved": "https://registry.npmjs.org/redux-test-utils/-/redux-test-utils-0.2.2.tgz",
|
||||
@ -26742,6 +26853,11 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"reselect": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/reselect/-/reselect-3.0.1.tgz",
|
||||
"integrity": "sha1-79qpjqdFEyTQkrKyFjpqHXqaIUc="
|
||||
},
|
||||
"resolve": {
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.4.0.tgz",
|
||||
|
@ -97,6 +97,7 @@
|
||||
"eth-hd-keyring": "^1.2.1",
|
||||
"eth-json-rpc-filters": "^1.2.6",
|
||||
"eth-json-rpc-infura": "^3.0.0",
|
||||
"eth-method-registry": "^1.0.0",
|
||||
"eth-phishing-detect": "^1.1.4",
|
||||
"eth-query": "^2.1.2",
|
||||
"eth-sig-util": "^1.4.2",
|
||||
@ -180,6 +181,7 @@
|
||||
"redux-logger": "^3.0.6",
|
||||
"redux-thunk": "^2.2.0",
|
||||
"request-promise": "^4.2.1",
|
||||
"reselect": "^3.0.1",
|
||||
"sandwich-expando": "^1.1.3",
|
||||
"semaphore": "^1.0.5",
|
||||
"semver": "^5.4.1",
|
||||
@ -286,6 +288,7 @@
|
||||
"react-addons-test-utils": "^15.5.1",
|
||||
"react-test-renderer": "^15.6.2",
|
||||
"react-testutils-additions": "^15.2.0",
|
||||
"redux-mock-store": "^1.5.3",
|
||||
"redux-test-utils": "^0.2.2",
|
||||
"resolve-url-loader": "^2.3.0",
|
||||
"rimraf": "^2.6.2",
|
||||
|
File diff suppressed because one or more lines are too long
@ -1,8 +1,33 @@
|
||||
<html>
|
||||
<head>
|
||||
<title>E2E Test Dapp</title>
|
||||
</head>
|
||||
<body>
|
||||
<button id="deployButton">Deploy Contract</button>
|
||||
<button id="depositButton">Deposit</button>
|
||||
<button id="withdrawButton">Withdraw</button>
|
||||
</body>
|
||||
<div style="display: flex; flex-flow: column;">
|
||||
<div style="display: flex; font-size: 1.25rem;">Contract</div>
|
||||
<div style="display: flex;">
|
||||
<button id="deployButton">Deploy Contract</button>
|
||||
<button id="depositButton">Deposit</button>
|
||||
<button id="withdrawButton">Withdraw</button>
|
||||
</div>
|
||||
</div>
|
||||
<div style="display: flex; flex-flow: column;">
|
||||
<div style="display: flex; font-size: 1.25rem;">Send eth</div>
|
||||
<div style="display: flex;">
|
||||
<button id="sendButton">Send</button>
|
||||
</div>
|
||||
</div>
|
||||
<div style="display: flex; flex-flow: column;">
|
||||
<div style="display: flex; font-size: 1.25rem;">Send tokens</div>
|
||||
<div id="tokenAddress"></div>
|
||||
<div style="display: flex;">
|
||||
<button id="createToken">Create Token</button>
|
||||
<button id="transferTokens">Transfer Tokens</button>
|
||||
<button id="approveTokens">Approve Tokens</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="contract.js"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
@ -1,16 +1,21 @@
|
||||
const fs = require('fs')
|
||||
const mkdirp = require('mkdirp')
|
||||
const pify = require('pify')
|
||||
const assert = require('assert')
|
||||
const {until} = require('selenium-webdriver')
|
||||
const { delay } = require('../func')
|
||||
|
||||
module.exports = {
|
||||
assertElementNotPresent,
|
||||
checkBrowserForConsoleErrors,
|
||||
loadExtension,
|
||||
verboseReportOnFailure,
|
||||
closeAllWindowHandlesExcept,
|
||||
findElement,
|
||||
findElements,
|
||||
loadExtension,
|
||||
openNewPage,
|
||||
switchToWindowWithTitle,
|
||||
verboseReportOnFailure,
|
||||
waitUntilXWindowHandles,
|
||||
}
|
||||
|
||||
async function loadExtension (driver, extensionId) {
|
||||
@ -72,9 +77,57 @@ async function openNewPage (driver, url) {
|
||||
await delay(1000)
|
||||
|
||||
const handles = await driver.getAllWindowHandles()
|
||||
const secondHandle = handles[1]
|
||||
await driver.switchTo().window(secondHandle)
|
||||
const lastHandle = handles[handles.length - 1]
|
||||
await driver.switchTo().window(lastHandle)
|
||||
|
||||
await driver.get(url)
|
||||
await delay(1000)
|
||||
}
|
||||
|
||||
async function waitUntilXWindowHandles (driver, x) {
|
||||
const windowHandles = await driver.getAllWindowHandles()
|
||||
if (windowHandles.length === x) return
|
||||
await delay(1000)
|
||||
return await waitUntilXWindowHandles(driver, x)
|
||||
}
|
||||
|
||||
async function switchToWindowWithTitle (driver, title, windowHandles) {
|
||||
if (!windowHandles) {
|
||||
windowHandles = await driver.getAllWindowHandles()
|
||||
} else if (windowHandles.length === 0) {
|
||||
throw new Error('No window with title: ' + title)
|
||||
}
|
||||
const firstHandle = windowHandles[0]
|
||||
await driver.switchTo().window(firstHandle)
|
||||
const handleTitle = await driver.getTitle()
|
||||
|
||||
if (handleTitle === title) {
|
||||
return firstHandle
|
||||
} else {
|
||||
return await switchToWindowWithTitle(driver, title, windowHandles.slice(1))
|
||||
}
|
||||
}
|
||||
|
||||
async function closeAllWindowHandlesExcept (driver, exceptions, windowHandles) {
|
||||
exceptions = typeof exceptions === 'string' ? [ exceptions ] : exceptions
|
||||
windowHandles = windowHandles || await driver.getAllWindowHandles()
|
||||
const lastWindowHandle = windowHandles.pop()
|
||||
if (!exceptions.includes(lastWindowHandle)) {
|
||||
await driver.switchTo().window(lastWindowHandle)
|
||||
await delay(1000)
|
||||
await driver.close()
|
||||
await delay(1000)
|
||||
}
|
||||
return windowHandles.length && await closeAllWindowHandlesExcept(driver, exceptions, windowHandles)
|
||||
}
|
||||
|
||||
async function assertElementNotPresent (webdriver, driver, by) {
|
||||
try {
|
||||
const dataTab = await findElement(driver, by, 4000)
|
||||
if (dataTab) {
|
||||
assert(false, 'Data tab should not be present')
|
||||
}
|
||||
} catch (err) {
|
||||
assert(err instanceof webdriver.error.NoSuchElementError)
|
||||
}
|
||||
}
|
||||
|
@ -11,12 +11,16 @@ const {
|
||||
getExtensionIdFirefox,
|
||||
} = require('../func')
|
||||
const {
|
||||
assertElementNotPresent,
|
||||
checkBrowserForConsoleErrors,
|
||||
closeAllWindowHandlesExcept,
|
||||
findElement,
|
||||
findElements,
|
||||
checkBrowserForConsoleErrors,
|
||||
loadExtension,
|
||||
verboseReportOnFailure,
|
||||
openNewPage,
|
||||
switchToWindowWithTitle,
|
||||
verboseReportOnFailure,
|
||||
waitUntilXWindowHandles,
|
||||
} = require('./helpers')
|
||||
|
||||
describe('MetaMask', function () {
|
||||
@ -25,7 +29,7 @@ describe('MetaMask', function () {
|
||||
let tokenAddress
|
||||
|
||||
const testSeedPhrase = 'phrase upgrade clock rough situate wedding elder clever doctor stamp excess tent'
|
||||
const tinyDelayMs = 1000
|
||||
const tinyDelayMs = 200
|
||||
const regularDelayMs = tinyDelayMs * 2
|
||||
const largeDelayMs = regularDelayMs * 2
|
||||
|
||||
@ -80,20 +84,29 @@ describe('MetaMask', function () {
|
||||
networkSelector = await findElement(driver, By.css('#network_component'))
|
||||
} catch (e) {
|
||||
await loadExtension(driver, extensionId)
|
||||
await delay(largeDelayMs * 2)
|
||||
networkSelector = await findElement(driver, By.css('#network_component'))
|
||||
}
|
||||
await delay(regularDelayMs)
|
||||
})
|
||||
|
||||
it('use the local network', async function () {
|
||||
it('uses the local network', async function () {
|
||||
await networkSelector.click()
|
||||
await delay(regularDelayMs)
|
||||
|
||||
const localhost = await findElement(driver, By.xpath(`//li[contains(text(), 'Localhost')]`))
|
||||
const networks = await findElements(driver, By.css('.dropdown-menu-item'))
|
||||
const localhost = networks[4]
|
||||
await driver.wait(until.elementTextMatches(localhost, /Localhost/))
|
||||
await localhost.click()
|
||||
await delay(regularDelayMs)
|
||||
})
|
||||
|
||||
it('selects the new UI option', async () => {
|
||||
try {
|
||||
const overlay = await findElement(driver, By.css('.full-flex-height'))
|
||||
await driver.wait(until.stalenessOf(overlay))
|
||||
} catch (e) {}
|
||||
|
||||
const button = await findElement(driver, By.xpath("//p[contains(text(), 'Try Beta Version')]"))
|
||||
await button.click()
|
||||
await delay(regularDelayMs)
|
||||
@ -188,8 +201,20 @@ describe('MetaMask', function () {
|
||||
await delay(regularDelayMs)
|
||||
})
|
||||
|
||||
async function retypeSeedPhrase (words) {
|
||||
async function retypeSeedPhrase (words, wasReloaded) {
|
||||
try {
|
||||
if (wasReloaded) {
|
||||
const byRevealButton = By.css('.backup-phrase__secret-blocker .backup-phrase__reveal-button')
|
||||
await driver.wait(until.elementLocated(byRevealButton, 10000))
|
||||
const revealSeedPhraseButton = await findElement(driver, byRevealButton, 10000)
|
||||
await revealSeedPhraseButton.click()
|
||||
await delay(regularDelayMs)
|
||||
|
||||
const nextScreen = await findElement(driver, By.css('.backup-phrase button'))
|
||||
await nextScreen.click()
|
||||
await delay(regularDelayMs)
|
||||
}
|
||||
|
||||
const word0 = await findElement(driver, By.xpath(`//button[contains(text(), '${words[0]}')]`), 10000)
|
||||
|
||||
await word0.click()
|
||||
@ -250,7 +275,7 @@ describe('MetaMask', function () {
|
||||
await delay(tinyDelayMs)
|
||||
} catch (e) {
|
||||
await loadExtension(driver, extensionId)
|
||||
await retypeSeedPhrase(words)
|
||||
await retypeSeedPhrase(words, true)
|
||||
}
|
||||
}
|
||||
|
||||
@ -303,7 +328,7 @@ describe('MetaMask', function () {
|
||||
it('accepts the account password after lock', async () => {
|
||||
await driver.findElement(By.id('password')).sendKeys('correct horse battery staple')
|
||||
await driver.findElement(By.id('password')).sendKeys(Key.ENTER)
|
||||
await delay(regularDelayMs * 4)
|
||||
await delay(largeDelayMs * 4)
|
||||
})
|
||||
})
|
||||
|
||||
@ -324,10 +349,10 @@ describe('MetaMask', function () {
|
||||
|
||||
const create = await findElement(driver, By.xpath(`//button[contains(text(), 'Create')]`))
|
||||
await create.click()
|
||||
await delay(regularDelayMs)
|
||||
await delay(largeDelayMs)
|
||||
})
|
||||
|
||||
it('should correct account name', async () => {
|
||||
it('should display correct account name', async () => {
|
||||
const accountName = await findElement(driver, By.css('.account-name'))
|
||||
assert.equal(await accountName.getText(), '2nd account')
|
||||
await delay(regularDelayMs)
|
||||
@ -366,8 +391,7 @@ describe('MetaMask', function () {
|
||||
|
||||
it('balance renders', async () => {
|
||||
const balance = await findElement(driver, By.css('.balance-display .token-amount'))
|
||||
const tokenAmount = await balance.getText()
|
||||
assert.equal(tokenAmount, '100.000 ETH')
|
||||
await driver.wait(until.elementTextMatches(balance, /100.+ETH/))
|
||||
await delay(regularDelayMs)
|
||||
})
|
||||
})
|
||||
@ -383,6 +407,9 @@ describe('MetaMask', function () {
|
||||
await inputAddress.sendKeys('0x2f318C334780961FB129D2a6c30D0763d9a5C970')
|
||||
await inputAmount.sendKeys('1')
|
||||
|
||||
const inputValue = await inputAmount.getAttribute('value')
|
||||
assert.equal(inputValue, '1')
|
||||
|
||||
// Set the gas limit
|
||||
const configureGas = await findElement(driver, By.css('.send-v2__gas-fee-display button'))
|
||||
await configureGas.click()
|
||||
@ -404,59 +431,71 @@ describe('MetaMask', function () {
|
||||
it('confirms the transaction', async function () {
|
||||
const confirmButton = await findElement(driver, By.xpath(`//button[contains(text(), 'Confirm')]`))
|
||||
await confirmButton.click()
|
||||
await delay(regularDelayMs)
|
||||
await delay(largeDelayMs)
|
||||
})
|
||||
|
||||
it('finds the transaction in the transactions list', async function () {
|
||||
const transactions = await findElements(driver, By.css('.tx-list-item'))
|
||||
assert.equal(transactions.length, 1)
|
||||
|
||||
const txValues = await findElement(driver, By.css('.tx-list-value'))
|
||||
await driver.wait(until.elementTextMatches(txValues, /1\sETH/), 10000)
|
||||
if (process.env.SELENIUM_BROWSER !== 'firefox') {
|
||||
const txValues = await findElement(driver, By.css('.tx-list-value'))
|
||||
await driver.wait(until.elementTextMatches(txValues, /1\sETH/), 10000)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe('Send ETH from Faucet', () => {
|
||||
it('starts a send transaction inside Faucet', async () => {
|
||||
await openNewPage(driver, 'https://faucet.metamask.io')
|
||||
|
||||
const [extension, faucet] = await driver.getAllWindowHandles()
|
||||
await driver.switchTo().window(faucet)
|
||||
|
||||
const faucetPageTitle = await findElement(driver, By.css('.container-fluid'))
|
||||
await driver.wait(until.elementTextMatches(faucetPageTitle, /MetaMask/))
|
||||
describe('Send ETH from dapp', () => {
|
||||
it('starts a send transaction inside the dapp', async () => {
|
||||
await openNewPage(driver, 'http://127.0.0.1:8080/')
|
||||
await delay(regularDelayMs)
|
||||
|
||||
const send1eth = await findElement(driver, By.xpath(`//button[contains(text(), '10 ether')]`), 14000)
|
||||
await send1eth.click()
|
||||
await waitUntilXWindowHandles(driver, 2)
|
||||
let windowHandles = await driver.getAllWindowHandles()
|
||||
const extension = windowHandles[0]
|
||||
const dapp = windowHandles[1]
|
||||
|
||||
await driver.switchTo().window(dapp)
|
||||
await delay(regularDelayMs)
|
||||
|
||||
await driver.switchTo().window(extension)
|
||||
await loadExtension(driver, extensionId)
|
||||
const send3eth = await findElement(driver, By.xpath(`//button[contains(text(), 'Send')]`), 10000)
|
||||
await send3eth.click()
|
||||
await delay(regularDelayMs)
|
||||
|
||||
const confirmButton = await findElement(driver, By.xpath(`//button[contains(text(), 'Confirm')]`), 14000)
|
||||
windowHandles = await driver.getAllWindowHandles()
|
||||
await driver.switchTo().window(windowHandles[2])
|
||||
await delay(regularDelayMs)
|
||||
|
||||
assertElementNotPresent(webdriver, driver, By.xpath(`//li[contains(text(), 'Data')]`))
|
||||
|
||||
const confirmButton = await findElement(driver, By.xpath(`//button[contains(text(), 'Confirm')]`), 10000)
|
||||
await confirmButton.click()
|
||||
await delay(regularDelayMs)
|
||||
|
||||
await driver.switchTo().window(faucet)
|
||||
await delay(regularDelayMs)
|
||||
await driver.close()
|
||||
await delay(regularDelayMs)
|
||||
await closeAllWindowHandlesExcept(driver, [extension, dapp])
|
||||
await driver.switchTo().window(extension)
|
||||
await delay(regularDelayMs)
|
||||
await loadExtension(driver, extensionId)
|
||||
await delay(regularDelayMs)
|
||||
})
|
||||
|
||||
it('finds the transaction in the transactions list', async function () {
|
||||
const transactions = await findElements(driver, By.css('.tx-list-item'))
|
||||
assert.equal(transactions.length, 2)
|
||||
|
||||
const txValues = await findElement(driver, By.css('.tx-list-value'))
|
||||
await driver.wait(until.elementTextMatches(txValues, /3\sETH/), 10000)
|
||||
})
|
||||
})
|
||||
|
||||
describe('Deploy contract and call contract methods', () => {
|
||||
let extension
|
||||
let contractTestPage
|
||||
it('confirms a deploy contract transaction', async () => {
|
||||
await openNewPage(driver, 'http://127.0.0.1:8080/');
|
||||
let dapp
|
||||
it('creates a deploy contract transaction', async () => {
|
||||
const windowHandles = await driver.getAllWindowHandles()
|
||||
extension = windowHandles[0]
|
||||
dapp = windowHandles[1]
|
||||
await delay(tinyDelayMs)
|
||||
|
||||
[extension, contractTestPage] = await driver.getAllWindowHandles()
|
||||
await driver.switchTo().window(dapp)
|
||||
await delay(regularDelayMs)
|
||||
|
||||
const deployContractButton = await findElement(driver, By.css('#deployButton'))
|
||||
@ -466,10 +505,28 @@ describe('MetaMask', function () {
|
||||
await driver.switchTo().window(extension)
|
||||
await delay(regularDelayMs)
|
||||
|
||||
const txListItem = await findElement(driver, By.css('.tx-list-item'))
|
||||
const txListItem = await findElement(driver, By.xpath(`//span[contains(text(), 'Contract Deployment')]`))
|
||||
await txListItem.click()
|
||||
await delay(regularDelayMs)
|
||||
})
|
||||
|
||||
it('displays the contract creation data', async () => {
|
||||
const dataTab = await findElement(driver, By.xpath(`//li[contains(text(), 'Data')]`))
|
||||
dataTab.click()
|
||||
await (regularDelayMs)
|
||||
|
||||
await findElement(driver, By.xpath(`//div[contains(text(), '127.0.0.1')]`))
|
||||
|
||||
const confirmDataDiv = await findElement(driver, By.css('.confirm-page-container-content__data-box'))
|
||||
const confirmDataText = await confirmDataDiv.getText()
|
||||
assert.equal(confirmDataText.match(/0x608060405234801561001057600080fd5b5033600160006101000a81548173ffffffffffffffffffffffffffffffffffffffff/))
|
||||
|
||||
const detailsTab = await findElement(driver, By.xpath(`//li[contains(text(), 'Details')]`))
|
||||
detailsTab.click()
|
||||
await (regularDelayMs)
|
||||
})
|
||||
|
||||
it('confirms a deploy contract transaction', async () => {
|
||||
const confirmButton = await findElement(driver, By.xpath(`//button[contains(text(), 'Confirm')]`))
|
||||
await confirmButton.click()
|
||||
await delay(regularDelayMs)
|
||||
@ -479,10 +536,11 @@ describe('MetaMask', function () {
|
||||
|
||||
const txAccounts = await findElements(driver, By.css('.tx-list-account'))
|
||||
assert.equal(await txAccounts[0].getText(), 'Contract Deployment')
|
||||
await delay(regularDelayMs)
|
||||
})
|
||||
|
||||
it('calls and confirms a contract method where ETH is sent', async () => {
|
||||
await driver.switchTo().window(contractTestPage)
|
||||
await driver.switchTo().window(dapp)
|
||||
await delay(regularDelayMs)
|
||||
|
||||
const depositButton = await findElement(driver, By.css('#depositButton'))
|
||||
@ -492,17 +550,19 @@ describe('MetaMask', function () {
|
||||
await driver.switchTo().window(extension)
|
||||
await delay(regularDelayMs)
|
||||
|
||||
const txListItem = await findElement(driver, By.css('.tx-list-item'))
|
||||
await txListItem.click()
|
||||
await findElements(driver, By.css('.tx-list-pending-item-container'))
|
||||
const [txListValue] = await findElements(driver, By.css('.tx-list-value'))
|
||||
await driver.wait(until.elementTextMatches(txListValue, /4\sETH/), 10000)
|
||||
await txListValue.click()
|
||||
await delay(regularDelayMs)
|
||||
|
||||
// Set the gas limit
|
||||
const configureGas = await findElement(driver, By.css('.sliders-icon-container'))
|
||||
const configureGas = await findElement(driver, By.css('.confirm-detail-row__header-text--edit'))
|
||||
await configureGas.click()
|
||||
await delay(regularDelayMs)
|
||||
|
||||
const gasModal = await driver.findElement(By.css('span .modal'))
|
||||
await driver.wait(until.elementLocated(By.css('.send-v2__customize-gas__title')))
|
||||
await driver.wait(until.elementLocated(By.css('.customize-gas__title')))
|
||||
|
||||
const [gasPriceInput, gasLimitInput] = await findElements(driver, By.css('.customize-gas-input'))
|
||||
await gasPriceInput.clear()
|
||||
@ -524,7 +584,7 @@ describe('MetaMask', function () {
|
||||
await driver.wait(until.elementTextMatches(txStatuses[0], /Confirmed/))
|
||||
|
||||
const txValues = await findElement(driver, By.css('.tx-list-value'))
|
||||
await driver.wait(until.elementTextMatches(txValues, /3\sETH/), 10000)
|
||||
await driver.wait(until.elementTextMatches(txValues, /4\sETH/), 10000)
|
||||
|
||||
const txAccounts = await findElements(driver, By.css('.tx-list-account'))
|
||||
const firstTxAddress = await txAccounts[0].getText()
|
||||
@ -532,7 +592,7 @@ describe('MetaMask', function () {
|
||||
})
|
||||
|
||||
it('calls and confirms a contract method where ETH is received', async () => {
|
||||
await driver.switchTo().window(contractTestPage)
|
||||
await driver.switchTo().window(dapp)
|
||||
await delay(regularDelayMs)
|
||||
|
||||
const withdrawButton = await findElement(driver, By.css('#withdrawButton'))
|
||||
@ -556,38 +616,31 @@ describe('MetaMask', function () {
|
||||
const txValues = await findElement(driver, By.css('.tx-list-value'))
|
||||
await driver.wait(until.elementTextMatches(txValues, /0\sETH/), 10000)
|
||||
|
||||
await driver.switchTo().window(contractTestPage)
|
||||
await driver.close()
|
||||
await closeAllWindowHandlesExcept(driver, [extension, dapp])
|
||||
await driver.switchTo().window(extension)
|
||||
})
|
||||
|
||||
it('renders the correct ETH balance', async () => {
|
||||
const balance = await findElement(driver, By.css('.tx-view .balance-display .token-amount'))
|
||||
await driver.wait(until.elementTextMatches(balance, /^86.*ETH.*$/), 10000)
|
||||
const tokenAmount = await balance.getText()
|
||||
assert.ok(/^86.*ETH.*$/.test(tokenAmount))
|
||||
await delay(regularDelayMs)
|
||||
if (process.env.SELENIUM_BROWSER !== 'firefox') {
|
||||
await driver.wait(until.elementTextMatches(balance, /^92.*ETH.*$/), 10000)
|
||||
const tokenAmount = await balance.getText()
|
||||
assert.ok(/^92.*ETH.*$/.test(tokenAmount))
|
||||
await delay(regularDelayMs)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe('Add a custom token from TokenFactory', () => {
|
||||
describe('Add a custom token from a dapp', () => {
|
||||
it('creates a new token', async () => {
|
||||
openNewPage(driver, 'https://tokenfactory.surge.sh/#/factory')
|
||||
const windowHandles = await driver.getAllWindowHandles()
|
||||
const extension = windowHandles[0]
|
||||
const dapp = windowHandles[1]
|
||||
await delay(regularDelayMs * 2)
|
||||
|
||||
await delay(regularDelayMs * 10)
|
||||
const [extension, tokenFactory] = await driver.getAllWindowHandles()
|
||||
|
||||
const [
|
||||
totalSupply,
|
||||
tokenName,
|
||||
tokenDecimal,
|
||||
tokenSymbol,
|
||||
] = await findElements(driver, By.css('.form-control'))
|
||||
|
||||
await totalSupply.sendKeys('100')
|
||||
await tokenName.sendKeys('Test')
|
||||
await tokenDecimal.sendKeys('0')
|
||||
await tokenSymbol.sendKeys('TST')
|
||||
await driver.switchTo().window(dapp)
|
||||
await delay(regularDelayMs)
|
||||
|
||||
const createToken = await findElement(driver, By.xpath(`//button[contains(text(), 'Create Token')]`))
|
||||
await createToken.click()
|
||||
@ -601,15 +654,16 @@ describe('MetaMask', function () {
|
||||
await confirmButton.click()
|
||||
await delay(regularDelayMs)
|
||||
|
||||
await driver.switchTo().window(tokenFactory)
|
||||
await driver.switchTo().window(dapp)
|
||||
await delay(tinyDelayMs)
|
||||
|
||||
const tokenContractAddress = await driver.findElement(By.css('#tokenAddress'))
|
||||
await driver.wait(until.elementTextMatches(tokenContractAddress, /0x/))
|
||||
tokenAddress = await tokenContractAddress.getText()
|
||||
|
||||
await delay(regularDelayMs)
|
||||
await closeAllWindowHandlesExcept(driver, [extension, dapp])
|
||||
await delay(regularDelayMs)
|
||||
|
||||
const tokenContactAddress = await driver.findElement(By.css('div > div > div:nth-child(2) > span:nth-child(3)'))
|
||||
tokenAddress = await tokenContactAddress.getText()
|
||||
|
||||
await driver.close()
|
||||
await driver.switchTo().window(extension)
|
||||
await loadExtension(driver, extensionId)
|
||||
await driver.switchTo().window(extension)
|
||||
await delay(regularDelayMs)
|
||||
|
||||
@ -668,7 +722,7 @@ describe('MetaMask', function () {
|
||||
gasModal = await driver.findElement(By.css('span .modal'))
|
||||
})
|
||||
|
||||
it('customizes gas', async () => {
|
||||
it('opens customizes gas modal', async () => {
|
||||
await driver.wait(until.elementLocated(By.css('.send-v2__customize-gas__title')))
|
||||
const save = await findElement(driver, By.xpath(`//button[contains(text(), 'Save')]`))
|
||||
await save.click()
|
||||
@ -684,6 +738,24 @@ describe('MetaMask', function () {
|
||||
await delay(regularDelayMs)
|
||||
})
|
||||
|
||||
it('displays the token transfer data', async () => {
|
||||
const dataTab = await findElement(driver, By.xpath(`//li[contains(text(), 'Data')]`))
|
||||
dataTab.click()
|
||||
await (regularDelayMs)
|
||||
|
||||
const functionType = await findElement(driver, By.css('.confirm-page-container-content__function-type'))
|
||||
const functionTypeText = await functionType.getText()
|
||||
assert.equal(functionTypeText, 'Transfer')
|
||||
|
||||
const confirmDataDiv = await findElement(driver, By.css('.confirm-page-container-content__data-box'))
|
||||
const confirmDataText = await confirmDataDiv.getText()
|
||||
assert.equal(confirmDataText.match(/0xa9059cbb0000000000000000000000002f318c334780961fb129d2a6c30d0763d9a5c97/))
|
||||
|
||||
const detailsTab = await findElement(driver, By.xpath(`//li[contains(text(), 'Details')]`))
|
||||
detailsTab.click()
|
||||
await (regularDelayMs)
|
||||
})
|
||||
|
||||
it('submits the transaction', async function () {
|
||||
const confirmButton = await findElement(driver, By.xpath(`//button[contains(text(), 'Confirm')]`))
|
||||
await confirmButton.click()
|
||||
@ -709,37 +781,33 @@ describe('MetaMask', function () {
|
||||
})
|
||||
})
|
||||
|
||||
describe('Send a custom token from TokenFactory', () => {
|
||||
describe('Send a custom token from dapp', () => {
|
||||
let gasModal
|
||||
it('sends an already created token', async () => {
|
||||
openNewPage(driver, `https://tokenfactory.surge.sh/#/token/${tokenAddress}`)
|
||||
|
||||
const [extension] = await driver.getAllWindowHandles()
|
||||
|
||||
const [
|
||||
transferToAddress,
|
||||
transferToAmount,
|
||||
] = await findElements(driver, By.css('.form-control'))
|
||||
|
||||
await transferToAddress.sendKeys('0x2f318C334780961FB129D2a6c30D0763d9a5C970')
|
||||
await transferToAmount.sendKeys('26')
|
||||
|
||||
const transferAmountButton = await findElement(driver, By.xpath(`//button[contains(text(), 'Transfer Amount')]`))
|
||||
await transferAmountButton.click()
|
||||
const windowHandles = await driver.getAllWindowHandles()
|
||||
const extension = windowHandles[0]
|
||||
const dapp = await switchToWindowWithTitle(driver, 'E2E Test Dapp', windowHandles)
|
||||
await closeAllWindowHandlesExcept(driver, [extension, dapp])
|
||||
await delay(regularDelayMs)
|
||||
|
||||
const [,, popup] = await driver.getAllWindowHandles()
|
||||
await driver.switchTo().window(popup)
|
||||
await driver.close()
|
||||
await driver.switchTo().window(dapp)
|
||||
await delay(tinyDelayMs)
|
||||
|
||||
const transferTokens = await findElement(driver, By.xpath(`//button[contains(text(), 'Transfer Tokens')]`))
|
||||
await transferTokens.click()
|
||||
|
||||
await closeAllWindowHandlesExcept(driver, [extension, dapp])
|
||||
await driver.switchTo().window(extension)
|
||||
await delay(regularDelayMs)
|
||||
await delay(largeDelayMs)
|
||||
|
||||
const [txListItem] = await findElements(driver, By.css('.tx-list-item'))
|
||||
await txListItem.click()
|
||||
await findElements(driver, By.css('.tx-list-pending-item-container'))
|
||||
const [txListValue] = await findElements(driver, By.css('.tx-list-value'))
|
||||
await driver.wait(until.elementTextMatches(txListValue, /7\sTST/), 10000)
|
||||
await txListValue.click()
|
||||
await delay(regularDelayMs)
|
||||
|
||||
// Set the gas limit
|
||||
const configureGas = await driver.wait(until.elementLocated(By.css('.send-v2__gas-fee-display button')))
|
||||
const configureGas = await driver.wait(until.elementLocated(By.css('.confirm-detail-row__header-text--edit')), 10000)
|
||||
await configureGas.click()
|
||||
await delay(regularDelayMs)
|
||||
|
||||
@ -747,7 +815,7 @@ describe('MetaMask', function () {
|
||||
})
|
||||
|
||||
it('customizes gas', async () => {
|
||||
await driver.wait(until.elementLocated(By.css('.send-v2__customize-gas__title')))
|
||||
await driver.wait(until.elementLocated(By.css('.customize-gas__title')))
|
||||
|
||||
const [gasPriceInput, gasLimitInput] = await findElements(driver, By.css('.customize-gas-input'))
|
||||
await gasPriceInput.clear()
|
||||
@ -766,12 +834,12 @@ describe('MetaMask', function () {
|
||||
await gasLimitInput.sendKeys(Key.BACK_SPACE)
|
||||
}
|
||||
|
||||
const save = await findElement(driver, By.css('.send-v2__customize-gas__save'))
|
||||
const save = await findElement(driver, By.css('.customize-gas__save'))
|
||||
await save.click()
|
||||
await driver.wait(until.stalenessOf(gasModal))
|
||||
|
||||
const gasFeeInput = await findElement(driver, By.css('.currency-display__input'))
|
||||
assert.equal(await gasFeeInput.getAttribute('value'), 0.0006)
|
||||
const gasFeeInputs = await findElements(driver, By.css('.confirm-detail-row__eth'))
|
||||
assert.equal(await gasFeeInputs[0].getText(), '♦ 0.0006')
|
||||
})
|
||||
|
||||
it('submits the transaction', async function () {
|
||||
@ -785,7 +853,7 @@ describe('MetaMask', function () {
|
||||
assert.equal(transactions.length, 2)
|
||||
|
||||
const txValues = await findElements(driver, By.css('.tx-list-value'))
|
||||
await driver.wait(until.elementTextMatches(txValues[0], /26\sTST/))
|
||||
await driver.wait(until.elementTextMatches(txValues[0], /7\sTST/))
|
||||
const txStatuses = await findElements(driver, By.css('.tx-list-status'))
|
||||
await driver.wait(until.elementTextMatches(txStatuses[0], /Confirmed/))
|
||||
|
||||
@ -799,11 +867,110 @@ describe('MetaMask', function () {
|
||||
// or possibly until we use latest version of firefox in the tests
|
||||
if (process.env.SELENIUM_BROWSER !== 'firefox') {
|
||||
const tokenBalanceAmount = await findElement(driver, By.css('.token-balance__amount'))
|
||||
assert.equal(await tokenBalanceAmount.getText(), '24')
|
||||
assert.equal(await tokenBalanceAmount.getText(), '43')
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe('Approves a custom token from dapp', () => {
|
||||
let gasModal
|
||||
it('approves an already created token', async () => {
|
||||
const windowHandles = await driver.getAllWindowHandles()
|
||||
const extension = windowHandles[0]
|
||||
const dapp = await switchToWindowWithTitle(driver, 'E2E Test Dapp', windowHandles)
|
||||
await closeAllWindowHandlesExcept(driver, [extension, dapp])
|
||||
await delay(regularDelayMs)
|
||||
|
||||
await driver.switchTo().window(dapp)
|
||||
await delay(tinyDelayMs)
|
||||
|
||||
const transferTokens = await findElement(driver, By.xpath(`//button[contains(text(), 'Approve Tokens')]`))
|
||||
await transferTokens.click()
|
||||
|
||||
await closeAllWindowHandlesExcept(driver, extension)
|
||||
await driver.switchTo().window(extension)
|
||||
await delay(regularDelayMs)
|
||||
|
||||
const [txListItem] = await findElements(driver, By.css('.tx-list-item'))
|
||||
const [txListValue] = await findElements(driver, By.css('.tx-list-value'))
|
||||
await driver.wait(until.elementTextMatches(txListValue, /0\sETH/))
|
||||
await txListItem.click()
|
||||
await delay(regularDelayMs)
|
||||
})
|
||||
|
||||
it('displays the token approval data', async () => {
|
||||
const dataTab = await findElement(driver, By.xpath(`//li[contains(text(), 'Data')]`))
|
||||
dataTab.click()
|
||||
await (regularDelayMs)
|
||||
|
||||
const functionType = await findElement(driver, By.css('.confirm-page-container-content__function-type'))
|
||||
const functionTypeText = await functionType.getText()
|
||||
assert.equal(functionTypeText, 'Approve')
|
||||
|
||||
const confirmDataDiv = await findElement(driver, By.css('.confirm-page-container-content__data-box'))
|
||||
const confirmDataText = await confirmDataDiv.getText()
|
||||
assert.equal(confirmDataText.match(/0x095ea7b30000000000000000000000002f318c334780961fb129d2a6c30d0763d9a5c97/))
|
||||
|
||||
const detailsTab = await findElement(driver, By.xpath(`//li[contains(text(), 'Details')]`))
|
||||
detailsTab.click()
|
||||
await (regularDelayMs)
|
||||
|
||||
const approvalWarning = await findElement(driver, By.css('.confirm-page-container-warning__warning'))
|
||||
const approvalWarningText = await approvalWarning.getText()
|
||||
assert(approvalWarningText.match(/By approving this/))
|
||||
await (regularDelayMs)
|
||||
})
|
||||
|
||||
it('opens the gas edit modal', async () => {
|
||||
const configureGas = await driver.wait(until.elementLocated(By.css('.confirm-detail-row__header-text--edit')))
|
||||
await configureGas.click()
|
||||
await delay(regularDelayMs)
|
||||
|
||||
gasModal = await driver.findElement(By.css('span .modal'))
|
||||
})
|
||||
|
||||
it('customizes gas', async () => {
|
||||
await driver.wait(until.elementLocated(By.css('.customize-gas__title')))
|
||||
|
||||
const [gasPriceInput, gasLimitInput] = await findElements(driver, By.css('.customize-gas-input'))
|
||||
await gasPriceInput.clear()
|
||||
await delay(tinyDelayMs)
|
||||
await gasPriceInput.sendKeys('10')
|
||||
await delay(tinyDelayMs)
|
||||
await gasLimitInput.clear()
|
||||
await delay(tinyDelayMs)
|
||||
await gasLimitInput.sendKeys(Key.chord(Key.CONTROL, 'a'))
|
||||
await gasLimitInput.sendKeys('60000')
|
||||
await gasLimitInput.sendKeys(Key.chord(Key.CONTROL, 'e'))
|
||||
|
||||
// Needed for different behaviour of input in different versions of firefox
|
||||
const gasLimitInputValue = await gasLimitInput.getAttribute('value')
|
||||
if (gasLimitInputValue === '600001') {
|
||||
await gasLimitInput.sendKeys(Key.BACK_SPACE)
|
||||
}
|
||||
|
||||
const save = await findElement(driver, By.css('.customize-gas__save'))
|
||||
await save.click()
|
||||
await driver.wait(until.stalenessOf(gasModal))
|
||||
|
||||
const gasFeeInputs = await findElements(driver, By.css('.confirm-detail-row__eth'))
|
||||
assert.equal(await gasFeeInputs[0].getText(), '♦ 0.0006')
|
||||
})
|
||||
|
||||
it('submits the transaction', async function () {
|
||||
const confirmButton = await findElement(driver, By.xpath(`//button[contains(text(), 'Confirm')]`))
|
||||
await confirmButton.click()
|
||||
await delay(regularDelayMs)
|
||||
})
|
||||
|
||||
it('finds the transaction in the transactions list', async function () {
|
||||
const txValues = await findElements(driver, By.css('.tx-list-value'))
|
||||
await driver.wait(until.elementTextMatches(txValues[0], /0\sETH/))
|
||||
const txStatuses = await findElements(driver, By.css('.tx-list-status'))
|
||||
await driver.wait(until.elementTextMatches(txStatuses[0], /Confirmed/))
|
||||
})
|
||||
})
|
||||
|
||||
describe('Hide token', () => {
|
||||
it('hides the token when clicked', async () => {
|
||||
const [hideTokenEllipsis] = await findElements(driver, By.css('.token-list-item__ellipsis'))
|
||||
|
@ -234,7 +234,7 @@ describe('Metamask popup page', function () {
|
||||
|
||||
submitButton.click()
|
||||
|
||||
await delay(500)
|
||||
await delay(1500)
|
||||
})
|
||||
|
||||
it('finds the transaction in the transactions list', async function () {
|
||||
|
@ -1,5 +1,6 @@
|
||||
const reactTriggerChange = require('react-trigger-change')
|
||||
const {
|
||||
timeout,
|
||||
queryAsync,
|
||||
} = require('../../lib/util')
|
||||
|
||||
@ -18,14 +19,14 @@ async function runConfirmSigRequestsTest (assert, done) {
|
||||
selectState.val('confirm sig requests')
|
||||
reactTriggerChange(selectState[0])
|
||||
|
||||
// await timeout(1000000)
|
||||
|
||||
const pendingRequestItem = $.find('.tx-list-item.tx-list-pending-item-container.tx-list-clickable')
|
||||
|
||||
if (pendingRequestItem[0]) {
|
||||
pendingRequestItem[0].click()
|
||||
}
|
||||
|
||||
await timeout(1000)
|
||||
|
||||
let confirmSigHeadline = await queryAsync($, '.request-signature__headline')
|
||||
assert.equal(confirmSigHeadline[0].textContent, 'Your signature is being requested')
|
||||
|
||||
@ -37,7 +38,7 @@ async function runConfirmSigRequestsTest (assert, done) {
|
||||
|
||||
let confirmSigSignButton = await queryAsync($, 'button.btn-primary.btn--large')
|
||||
confirmSigSignButton[0].click()
|
||||
|
||||
await timeout(1000)
|
||||
confirmSigHeadline = await queryAsync($, '.request-signature__headline')
|
||||
assert.equal(confirmSigHeadline[0].textContent, 'Your signature is being requested')
|
||||
|
||||
@ -46,7 +47,7 @@ async function runConfirmSigRequestsTest (assert, done) {
|
||||
|
||||
confirmSigSignButton = await queryAsync($, 'button.btn-primary.btn--large')
|
||||
confirmSigSignButton[0].click()
|
||||
|
||||
await timeout(1000)
|
||||
confirmSigHeadline = await queryAsync($, '.request-signature__headline')
|
||||
assert.equal(confirmSigHeadline[0].textContent, 'Your signature is being requested')
|
||||
|
||||
@ -57,6 +58,5 @@ async function runConfirmSigRequestsTest (assert, done) {
|
||||
confirmSigSignButton = await queryAsync($, 'button.btn-primary.btn--large')
|
||||
confirmSigSignButton[0].click()
|
||||
|
||||
const txView = await queryAsync($, '.tx-view')
|
||||
assert.ok(txView[0], 'Should return to the account details screen after confirming')
|
||||
await timeout(2000)
|
||||
}
|
||||
|
@ -19,6 +19,7 @@ async function runCurrencyLocalizationTest (assert, done) {
|
||||
console.log('*** start runCurrencyLocalizationTest')
|
||||
const selectState = await queryAsync($, 'select')
|
||||
selectState.val('currency localization')
|
||||
await timeout(1000)
|
||||
reactTriggerChange(selectState[0])
|
||||
await timeout(1000)
|
||||
const txView = await queryAsync($, '.tx-view')
|
||||
|
@ -125,18 +125,18 @@ async function runSendFlowTest (assert, done) {
|
||||
reactTriggerChange(selectState[0])
|
||||
|
||||
const confirmFromName = (await queryAsync($, '.sender-to-recipient__sender-name')).first()
|
||||
assert.equal(confirmFromName[0].textContent, 'Send Account 2', 'confirm screen should show correct from name')
|
||||
assert.equal(confirmFromName[0].textContent, 'Send Account 4', 'confirm screen should show correct from name')
|
||||
|
||||
const confirmToName = (await queryAsync($, '.sender-to-recipient__recipient-name')).last()
|
||||
assert.equal(confirmToName[0].textContent, 'Send Account 3', 'confirm screen should show correct to name')
|
||||
|
||||
const confirmScreenRows = await queryAsync($, '.confirm-screen-rows')
|
||||
const confirmScreenGas = confirmScreenRows.find('.currency-display__converted-value')[0]
|
||||
assert.equal(confirmScreenGas.textContent, '$3.60 USD', 'confirm screen should show correct gas')
|
||||
const confirmScreenTotal = confirmScreenRows.find('.confirm-screen-row-info')[2]
|
||||
assert.equal(confirmScreenTotal.textContent, '$2,405.36 USD', 'confirm screen should show correct total')
|
||||
const confirmScreenRowFiats = await queryAsync($, '.confirm-detail-row__fiat')
|
||||
const confirmScreenGas = confirmScreenRowFiats[0]
|
||||
assert.equal(confirmScreenGas.textContent, '$3.60', 'confirm screen should show correct gas')
|
||||
const confirmScreenTotal = confirmScreenRowFiats[1]
|
||||
assert.equal(confirmScreenTotal.textContent, '$2,405.36', 'confirm screen should show correct total')
|
||||
|
||||
const confirmScreenBackButton = await queryAsync($, '.page-container__back-button')
|
||||
const confirmScreenBackButton = await queryAsync($, '.confirm-page-container-header__back-button')
|
||||
confirmScreenBackButton[0].click()
|
||||
|
||||
const sendFromFieldItemInEdit = await queryAsync($, '.account-list-item')
|
||||
|
@ -1,74 +1,54 @@
|
||||
// var jsdom = require('mocha-jsdom')
|
||||
var assert = require('assert')
|
||||
var freeze = require('deep-freeze-strict')
|
||||
var path = require('path')
|
||||
var sinon = require('sinon')
|
||||
|
||||
var actions = require(path.join(__dirname, '..', '..', '..', 'ui', 'app', 'actions.js'))
|
||||
var reducers = require(path.join(__dirname, '..', '..', '..', 'ui', 'app', 'reducers.js'))
|
||||
import configureMockStore from 'redux-mock-store'
|
||||
import thunk from 'redux-thunk'
|
||||
|
||||
const actions = require(path.join(__dirname, '../../../ui/app/actions.js'))
|
||||
|
||||
const middlewares = [thunk]
|
||||
const mockStore = configureMockStore(middlewares)
|
||||
|
||||
describe('tx confirmation screen', function () {
|
||||
beforeEach(function () {
|
||||
this.sinon = sinon.createSandbox()
|
||||
})
|
||||
|
||||
afterEach(function () {
|
||||
this.sinon.restore()
|
||||
})
|
||||
|
||||
var initialState, result
|
||||
|
||||
describe('when there is only one tx', function () {
|
||||
var firstTxId = 1457634084250832
|
||||
|
||||
beforeEach(function () {
|
||||
initialState = {
|
||||
appState: {
|
||||
currentView: {
|
||||
name: 'confTx',
|
||||
},
|
||||
const txId = 1457634084250832
|
||||
const initialState = {
|
||||
appState: {
|
||||
currentView: {
|
||||
name: 'confTx',
|
||||
},
|
||||
},
|
||||
metamask: {
|
||||
unapprovedTxs: {
|
||||
[txId]: {
|
||||
id: txId,
|
||||
status: 'unconfirmed',
|
||||
time: 1457634084250,
|
||||
},
|
||||
metamask: {
|
||||
unapprovedTxs: {
|
||||
'1457634084250832': {
|
||||
id: 1457634084250832,
|
||||
status: 'unconfirmed',
|
||||
time: 1457634084250,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
freeze(initialState)
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
const store = mockStore(initialState)
|
||||
|
||||
describe('cancelTx', function () {
|
||||
before(function (done) {
|
||||
actions._setBackgroundConnection({
|
||||
approveTransaction (txId, cb) { cb('An error!') },
|
||||
cancelTransaction (txId, cb) { cb() },
|
||||
clearSeedWordCache (cb) { cb() },
|
||||
getState (cb) { cb() },
|
||||
})
|
||||
done()
|
||||
})
|
||||
|
||||
describe('cancelTx', function () {
|
||||
before(function (done) {
|
||||
actions._setBackgroundConnection({
|
||||
approveTransaction (txId, cb) { cb('An error!') },
|
||||
cancelTransaction (txId, cb) { cb() },
|
||||
clearSeedWordCache (cb) { cb() },
|
||||
it('creates COMPLETED_TX with the cancelled transaction ID', function (done) {
|
||||
store.dispatch(actions.cancelTx({ id: txId }))
|
||||
.then(() => {
|
||||
const storeActions = store.getActions()
|
||||
const completedTxAction = storeActions.find(({ type }) => type === actions.COMPLETED_TX)
|
||||
assert.equal(completedTxAction.value, txId)
|
||||
done()
|
||||
})
|
||||
|
||||
actions.cancelTx({value: firstTxId})((action) => {
|
||||
result = reducers(initialState, action)
|
||||
})
|
||||
done()
|
||||
})
|
||||
|
||||
it('should transition to the account detail view', function () {
|
||||
assert.equal(result.appState.currentView.name, 'accountDetail')
|
||||
})
|
||||
|
||||
it('should have no unconfirmed txs remaining', function () {
|
||||
var count = getUnconfirmedTxCount(result)
|
||||
assert.equal(count, 0)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
function getUnconfirmedTxCount (state) {
|
||||
var txs = state.metamask.unapprovedTxs
|
||||
var count = Object.keys(txs).length
|
||||
return count
|
||||
}
|
||||
|
@ -353,9 +353,16 @@ describe('Transaction Controller', function () {
|
||||
])
|
||||
})
|
||||
|
||||
it('should set the transaction to rejected from unapproved', async function () {
|
||||
await txController.cancelTransaction(0)
|
||||
assert.equal(txController.txStateManager.getTx(0).status, 'rejected')
|
||||
it('should emit a status change to rejected', function (done) {
|
||||
txController.once('tx:status-update', (txId, status) => {
|
||||
try {
|
||||
assert.equal(status, 'rejected', 'status should e rejected')
|
||||
assert.equal(txId, 0, 'id should e 0')
|
||||
done()
|
||||
} catch (e) { done(e) }
|
||||
})
|
||||
|
||||
txController.cancelTransaction(0)
|
||||
})
|
||||
|
||||
})
|
||||
|
@ -43,14 +43,13 @@ describe('TransactionStateManager', function () {
|
||||
})
|
||||
|
||||
describe('#setTxStatusRejected', function () {
|
||||
it('sets the tx status to rejected', function () {
|
||||
it('sets the tx status to rejected and removes it from history', function () {
|
||||
const tx = { id: 1, status: 'unapproved', metamaskNetworkId: currentNetworkId, txParams: {} }
|
||||
txStateManager.addTx(tx)
|
||||
txStateManager.setTxStatusRejected(1)
|
||||
const result = txStateManager.getTxList()
|
||||
assert.ok(Array.isArray(result))
|
||||
assert.equal(result.length, 1)
|
||||
assert.equal(result[0].status, 'rejected')
|
||||
assert.equal(result.length, 0)
|
||||
})
|
||||
|
||||
it('should emit a rejected event to signal the exciton of callback', (done) => {
|
||||
@ -287,4 +286,18 @@ describe('TransactionStateManager', function () {
|
||||
|
||||
})
|
||||
})
|
||||
|
||||
describe('#_removeTx', function () {
|
||||
it('should remove the transaction from the storage', () => {
|
||||
txStateManager._saveTxList([ {id: 1} ])
|
||||
txStateManager._removeTx(1)
|
||||
assert(!txStateManager.getFullTxList().length, 'txList should be empty')
|
||||
})
|
||||
|
||||
it('should only remove the transaction with ID 1 from the storage', () => {
|
||||
txStateManager._saveTxList([ {id: 1}, {id: 2} ])
|
||||
txStateManager._removeTx(1)
|
||||
assert.equal(txStateManager.getFullTxList()[0].id, 2, 'txList should have a id of 2')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
50
test/unit/migrations/027-test.js
Normal file
50
test/unit/migrations/027-test.js
Normal file
@ -0,0 +1,50 @@
|
||||
const assert = require('assert')
|
||||
const migration27 = require('../../../app/scripts/migrations/027')
|
||||
|
||||
const oldStorage = {
|
||||
'meta': {},
|
||||
'data': {
|
||||
'TransactionController': {
|
||||
'transactions': [
|
||||
],
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
const transactions = []
|
||||
|
||||
|
||||
while (transactions.length < 9) {
|
||||
transactions.push({status: 'rejected'})
|
||||
transactions.push({status: 'unapproved'})
|
||||
transactions.push({status: 'approved'})
|
||||
}
|
||||
|
||||
|
||||
oldStorage.data.TransactionController.transactions = transactions
|
||||
|
||||
describe('migration #27', () => {
|
||||
it('should remove rejected transactions', (done) => {
|
||||
migration27.migrate(oldStorage)
|
||||
.then((newStorage) => {
|
||||
const newTransactions = newStorage.data.TransactionController.transactions
|
||||
assert.equal(newTransactions.length, 6, 'transactions is expected to have the length of 6')
|
||||
newTransactions.forEach((txMeta) => {
|
||||
if (txMeta.status === 'rejected') done(new Error('transaction was found with a status of rejected'))
|
||||
})
|
||||
done()
|
||||
})
|
||||
.catch(done)
|
||||
})
|
||||
|
||||
it('should successfully migrate first time state', (done) => {
|
||||
migration27.migrate({
|
||||
meta: {},
|
||||
data: require('../../../app/scripts/first-time-state'),
|
||||
})
|
||||
.then((migratedData) => {
|
||||
assert.equal(migratedData.meta.version, migration27.version)
|
||||
done()
|
||||
}).catch(done)
|
||||
})
|
||||
})
|
@ -811,11 +811,10 @@ function signTypedMsg (msgData) {
|
||||
|
||||
function signTx (txData) {
|
||||
return (dispatch) => {
|
||||
dispatch(actions.showLoadingIndication())
|
||||
global.ethQuery.sendTransaction(txData, (err, data) => {
|
||||
dispatch(actions.hideLoadingIndication())
|
||||
if (err) return dispatch(actions.displayWarning(err.message))
|
||||
dispatch(actions.hideWarning())
|
||||
if (err) {
|
||||
return dispatch(actions.displayWarning(err.message))
|
||||
}
|
||||
})
|
||||
dispatch(actions.showConfTxPage({}))
|
||||
}
|
||||
@ -1017,29 +1016,41 @@ function signTokenTx (tokenAddress, toAddress, amount, txData) {
|
||||
|
||||
function updateTransaction (txData) {
|
||||
log.info('actions: updateTx: ' + JSON.stringify(txData))
|
||||
return (dispatch) => {
|
||||
return dispatch => {
|
||||
log.debug(`actions calling background.updateTx`)
|
||||
background.updateTransaction(txData, (err) => {
|
||||
dispatch(actions.hideLoadingIndication())
|
||||
dispatch(actions.updateTransactionParams(txData.id, txData.txParams))
|
||||
if (err) {
|
||||
dispatch(actions.txError(err))
|
||||
dispatch(actions.goHome())
|
||||
return log.error(err.message)
|
||||
}
|
||||
dispatch(actions.showConfTxPage({ id: txData.id }))
|
||||
dispatch(actions.showLoadingIndication())
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
background.updateTransaction(txData, (err) => {
|
||||
dispatch(actions.updateTransactionParams(txData.id, txData.txParams))
|
||||
if (err) {
|
||||
dispatch(actions.txError(err))
|
||||
dispatch(actions.goHome())
|
||||
log.error(err.message)
|
||||
return reject(err)
|
||||
}
|
||||
|
||||
resolve(txData)
|
||||
})
|
||||
})
|
||||
.then(() => updateMetamaskStateFromBackground())
|
||||
.then(newState => dispatch(actions.updateMetamaskState(newState)))
|
||||
.then(() => {
|
||||
dispatch(actions.showConfTxPage({ id: txData.id }))
|
||||
dispatch(actions.hideLoadingIndication())
|
||||
return txData
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
function updateAndApproveTx (txData) {
|
||||
log.info('actions: updateAndApproveTx: ' + JSON.stringify(txData))
|
||||
return (dispatch) => {
|
||||
return dispatch => {
|
||||
log.debug(`actions calling background.updateAndApproveTx`)
|
||||
dispatch(actions.showLoadingIndication())
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
background.updateAndApproveTransaction(txData, err => {
|
||||
dispatch(actions.hideLoadingIndication())
|
||||
dispatch(actions.updateTransactionParams(txData.id, txData.txParams))
|
||||
dispatch(actions.clearSend())
|
||||
|
||||
@ -1050,10 +1061,17 @@ function updateAndApproveTx (txData) {
|
||||
reject(err)
|
||||
}
|
||||
|
||||
dispatch(actions.completedTx(txData.id))
|
||||
resolve(txData)
|
||||
})
|
||||
})
|
||||
.then(() => updateMetamaskStateFromBackground())
|
||||
.then(newState => dispatch(actions.updateMetamaskState(newState)))
|
||||
.then(() => {
|
||||
dispatch(actions.clearSend())
|
||||
dispatch(actions.completedTx(txData.id))
|
||||
dispatch(actions.hideLoadingIndication())
|
||||
return txData
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@ -1145,13 +1163,25 @@ function cancelTypedMsg (msgData) {
|
||||
function cancelTx (txData) {
|
||||
return dispatch => {
|
||||
log.debug(`background.cancelTransaction`)
|
||||
dispatch(actions.showLoadingIndication())
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
background.cancelTransaction(txData.id, () => {
|
||||
dispatch(actions.clearSend())
|
||||
dispatch(actions.completedTx(txData.id))
|
||||
resolve(txData)
|
||||
background.cancelTransaction(txData.id, err => {
|
||||
if (err) {
|
||||
return reject(err)
|
||||
}
|
||||
|
||||
resolve()
|
||||
})
|
||||
})
|
||||
.then(() => updateMetamaskStateFromBackground())
|
||||
.then(newState => dispatch(actions.updateMetamaskState(newState)))
|
||||
.then(() => {
|
||||
dispatch(actions.clearSend())
|
||||
dispatch(actions.completedTx(txData.id))
|
||||
dispatch(actions.hideLoadingIndication())
|
||||
return txData
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -12,7 +12,7 @@ const log = require('loglevel')
|
||||
const InitializeScreen = require('../../mascara/src/app/first-time').default
|
||||
// accounts
|
||||
const SendTransactionScreen = require('./components/send_/send.container')
|
||||
const ConfirmTxScreen = require('./conf-tx')
|
||||
const ConfirmTransaction = require('./components/pages/confirm-transaction')
|
||||
|
||||
// slideout menu
|
||||
const WalletView = require('./components/wallet-view')
|
||||
@ -22,7 +22,6 @@ const Home = require('./components/pages/home')
|
||||
const Authenticated = require('./components/pages/authenticated')
|
||||
const Initialized = require('./components/pages/initialized')
|
||||
const Settings = require('./components/pages/settings')
|
||||
const UnlockPage = require('./components/pages/unlock-page')
|
||||
const RestoreVaultPage = require('./components/pages/keychains/restore-vault').default
|
||||
const RevealSeedConfirmation = require('./components/pages/keychains/reveal-seed')
|
||||
const AddTokenPage = require('./components/pages/add-token')
|
||||
@ -40,6 +39,8 @@ const Modal = require('./components/modals/index').Modal
|
||||
|
||||
const AppHeader = require('./components/app-header')
|
||||
|
||||
import UnlockPage from './components/pages/unlock-page'
|
||||
|
||||
// Routes
|
||||
const {
|
||||
DEFAULT_ROUTE,
|
||||
@ -76,7 +77,10 @@ class App extends Component {
|
||||
h(Authenticated, { path: REVEAL_SEED_ROUTE, exact, component: RevealSeedConfirmation }),
|
||||
h(Authenticated, { path: SETTINGS_ROUTE, component: Settings }),
|
||||
h(Authenticated, { path: NOTICE_ROUTE, exact, component: NoticeScreen }),
|
||||
h(Authenticated, { path: `${CONFIRM_TRANSACTION_ROUTE}/:id?`, component: ConfirmTxScreen }),
|
||||
h(Authenticated, {
|
||||
path: `${CONFIRM_TRANSACTION_ROUTE}/:id?`,
|
||||
component: ConfirmTransaction,
|
||||
}),
|
||||
h(Authenticated, { path: SEND_ROUTE, exact, component: SendTransactionScreen }),
|
||||
h(Authenticated, { path: ADD_TOKEN_ROUTE, exact, component: AddTokenPage }),
|
||||
h(Authenticated, { path: CONFIRM_ADD_TOKEN_ROUTE, exact, component: ConfirmAddTokenPage }),
|
||||
|
@ -91,7 +91,6 @@ class AppHeader extends Component {
|
||||
network,
|
||||
provider,
|
||||
history,
|
||||
location,
|
||||
isUnlocked,
|
||||
} = this.props
|
||||
|
||||
@ -126,7 +125,7 @@ class AppHeader extends Component {
|
||||
network={network}
|
||||
provider={provider}
|
||||
onClick={event => this.handleNetworkIndicatorClick(event)}
|
||||
disabled={location.pathname === CONFIRM_TRANSACTION_ROUTE}
|
||||
disabled={this.isConfirming()}
|
||||
/>
|
||||
</div>
|
||||
{ this.renderAccountMenu() }
|
||||
|
@ -5,15 +5,24 @@ import classnames from 'classnames'
|
||||
const CLASSNAME_DEFAULT = 'btn-default'
|
||||
const CLASSNAME_PRIMARY = 'btn-primary'
|
||||
const CLASSNAME_SECONDARY = 'btn-secondary'
|
||||
const CLASSNAME_CONFIRM = 'btn-confirm'
|
||||
const CLASSNAME_LARGE = 'btn--large'
|
||||
|
||||
const typeHash = {
|
||||
default: CLASSNAME_DEFAULT,
|
||||
primary: CLASSNAME_PRIMARY,
|
||||
secondary: CLASSNAME_SECONDARY,
|
||||
confirm: CLASSNAME_CONFIRM,
|
||||
}
|
||||
|
||||
class Button extends Component {
|
||||
export default class Button extends Component {
|
||||
static propTypes = {
|
||||
type: PropTypes.string,
|
||||
large: PropTypes.bool,
|
||||
className: PropTypes.string,
|
||||
children: PropTypes.string,
|
||||
}
|
||||
|
||||
render () {
|
||||
const { type, large, className, ...buttonProps } = this.props
|
||||
|
||||
@ -31,13 +40,3 @@ class Button extends Component {
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Button.propTypes = {
|
||||
type: PropTypes.string,
|
||||
large: PropTypes.bool,
|
||||
className: PropTypes.string,
|
||||
children: PropTypes.string,
|
||||
}
|
||||
|
||||
export default Button
|
||||
|
||||
|
@ -0,0 +1,52 @@
|
||||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import classnames from 'classnames'
|
||||
|
||||
const ConfirmDetailRow = props => {
|
||||
const {
|
||||
label,
|
||||
fiatFee,
|
||||
ethFee,
|
||||
onHeaderClick,
|
||||
fiatFeeColor,
|
||||
headerText,
|
||||
headerTextClassName,
|
||||
} = props
|
||||
|
||||
return (
|
||||
<div className="confirm-detail-row">
|
||||
<div className="confirm-detail-row__label">
|
||||
{ label }
|
||||
</div>
|
||||
<div className="confirm-detail-row__details">
|
||||
<div
|
||||
className={classnames('confirm-detail-row__header-text', headerTextClassName)}
|
||||
onClick={() => onHeaderClick && onHeaderClick()}
|
||||
>
|
||||
{ headerText }
|
||||
</div>
|
||||
<div
|
||||
className="confirm-detail-row__fiat"
|
||||
style={{ color: fiatFeeColor }}
|
||||
>
|
||||
{ fiatFee }
|
||||
</div>
|
||||
<div className="confirm-detail-row__eth">
|
||||
{ `\u2666 ${ethFee}` }
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
ConfirmDetailRow.propTypes = {
|
||||
label: PropTypes.string,
|
||||
fiatFee: PropTypes.string,
|
||||
ethFee: PropTypes.string,
|
||||
fiatFeeColor: PropTypes.string,
|
||||
onHeaderClick: PropTypes.func,
|
||||
headerText: PropTypes.string,
|
||||
headerTextClassName: PropTypes.string,
|
||||
}
|
||||
|
||||
export default ConfirmDetailRow
|
@ -0,0 +1 @@
|
||||
export { default } from './confirm-detail-row.component'
|
@ -0,0 +1,43 @@
|
||||
.confirm-detail-row {
|
||||
padding: 14px 0;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
|
||||
&__label {
|
||||
font-size: .75rem;
|
||||
font-weight: 500;
|
||||
color: $scorpion;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
&__details {
|
||||
flex: 1;
|
||||
text-align: end;
|
||||
}
|
||||
|
||||
&__fiat {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
&__eth {
|
||||
color: $oslo-gray;
|
||||
}
|
||||
|
||||
&__header-text {
|
||||
font-size: .75rem;
|
||||
text-transform: uppercase;
|
||||
margin-bottom: 6px;
|
||||
color: $scorpion;
|
||||
|
||||
&--edit {
|
||||
color: $curious-blue;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
&--total {
|
||||
font-size: .625rem;
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,105 @@
|
||||
import React, { Component } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import classnames from 'classnames'
|
||||
import { Tabs, Tab } from '../../tabs'
|
||||
import {
|
||||
ConfirmPageContainerSummary,
|
||||
ConfirmPageContainerError,
|
||||
ConfirmPageContainerWarning,
|
||||
} from './'
|
||||
|
||||
export default class ConfirmPageContainerContent extends Component {
|
||||
static propTypes = {
|
||||
action: PropTypes.string,
|
||||
dataComponent: PropTypes.node,
|
||||
detailsComponent: PropTypes.node,
|
||||
errorKey: PropTypes.string,
|
||||
errorMessage: PropTypes.string,
|
||||
hideSubtitle: PropTypes.bool,
|
||||
identiconAddress: PropTypes.string,
|
||||
nonce: PropTypes.string,
|
||||
subtitle: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
|
||||
summaryComponent: PropTypes.node,
|
||||
title: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
|
||||
titleComponent: PropTypes.func,
|
||||
warning: PropTypes.string,
|
||||
}
|
||||
|
||||
renderContent () {
|
||||
const { detailsComponent, dataComponent } = this.props
|
||||
|
||||
if (detailsComponent && dataComponent) {
|
||||
return this.renderTabs()
|
||||
} else {
|
||||
return detailsComponent || dataComponent
|
||||
}
|
||||
}
|
||||
|
||||
renderTabs () {
|
||||
const { detailsComponent, dataComponent } = this.props
|
||||
|
||||
return (
|
||||
<Tabs>
|
||||
<Tab name="Details">
|
||||
{ detailsComponent }
|
||||
</Tab>
|
||||
<Tab name="Data">
|
||||
{ dataComponent }
|
||||
</Tab>
|
||||
</Tabs>
|
||||
)
|
||||
}
|
||||
|
||||
render () {
|
||||
const {
|
||||
action,
|
||||
errorKey,
|
||||
errorMessage,
|
||||
title,
|
||||
subtitle,
|
||||
hideSubtitle,
|
||||
identiconAddress,
|
||||
nonce,
|
||||
summaryComponent,
|
||||
detailsComponent,
|
||||
dataComponent,
|
||||
warning,
|
||||
} = this.props
|
||||
|
||||
return (
|
||||
<div className="confirm-page-container-content">
|
||||
{
|
||||
warning && (
|
||||
<ConfirmPageContainerWarning warning={warning} />
|
||||
)
|
||||
}
|
||||
{
|
||||
summaryComponent || (
|
||||
<ConfirmPageContainerSummary
|
||||
className={classnames({
|
||||
'confirm-page-container-summary--border': !detailsComponent || !dataComponent,
|
||||
})}
|
||||
action={action}
|
||||
title={title}
|
||||
subtitle={subtitle}
|
||||
hideSubtitle={hideSubtitle}
|
||||
identiconAddress={identiconAddress}
|
||||
nonce={nonce}
|
||||
/>
|
||||
)
|
||||
}
|
||||
{ this.renderContent() }
|
||||
{
|
||||
(errorKey || errorMessage) && (
|
||||
<div className="confirm-page-container-content__error-container">
|
||||
<ConfirmPageContainerError
|
||||
errorMessage={errorMessage}
|
||||
errorKey={errorKey}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
@ -0,0 +1,28 @@
|
||||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
|
||||
const ConfirmPageContainerError = (props, context) => {
|
||||
const { errorMessage, errorKey } = props
|
||||
const error = errorKey ? context.t(errorKey) : errorMessage
|
||||
|
||||
return (
|
||||
<div className="confirm-page-container-error">
|
||||
<img
|
||||
src="/images/alert-red.svg"
|
||||
className="confirm-page-container-error__icon"
|
||||
/>
|
||||
{ `ALERT: ${error}` }
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
ConfirmPageContainerError.propTypes = {
|
||||
errorMessage: PropTypes.string,
|
||||
errorKey: PropTypes.string,
|
||||
}
|
||||
|
||||
ConfirmPageContainerError.contextTypes = {
|
||||
t: PropTypes.func,
|
||||
}
|
||||
|
||||
export default ConfirmPageContainerError
|
@ -0,0 +1 @@
|
||||
export { default } from './confirm-page-container-error.component'
|
@ -0,0 +1,17 @@
|
||||
.confirm-page-container-error {
|
||||
height: 32px;
|
||||
border: 1px solid $monzo;
|
||||
color: $monzo;
|
||||
background: lighten($monzo, 56%);
|
||||
border-radius: 4px;
|
||||
font-size: .75rem;
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
padding-left: 16px;
|
||||
|
||||
&__icon {
|
||||
margin-right: 8px;
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
}
|
@ -0,0 +1,56 @@
|
||||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import classnames from 'classnames'
|
||||
import Identicon from '../../../identicon'
|
||||
|
||||
const ConfirmPageContainerSummary = props => {
|
||||
const { action, title, subtitle, hideSubtitle, className, identiconAddress, nonce } = props
|
||||
|
||||
return (
|
||||
<div className={classnames('confirm-page-container-summary', className)}>
|
||||
<div className="confirm-page-container-summary__action-row">
|
||||
<div className="confirm-page-container-summary__action">
|
||||
{ action }
|
||||
</div>
|
||||
{
|
||||
nonce && (
|
||||
<div className="confirm-page-container-summary__nonce">
|
||||
{ `#${nonce}` }
|
||||
</div>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
<div className="confirm-page-container-summary__title">
|
||||
{
|
||||
identiconAddress && (
|
||||
<Identicon
|
||||
className="confirm-page-container-summary__identicon"
|
||||
diameter={36}
|
||||
address={identiconAddress}
|
||||
/>
|
||||
)
|
||||
}
|
||||
<div className="confirm-page-container-summary__title-text">
|
||||
{ title }
|
||||
</div>
|
||||
</div>
|
||||
{
|
||||
hideSubtitle || <div className="confirm-page-container-summary__subtitle">
|
||||
{ subtitle }
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
ConfirmPageContainerSummary.propTypes = {
|
||||
action: PropTypes.string,
|
||||
title: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
|
||||
subtitle: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
|
||||
hideSubtitle: PropTypes.bool,
|
||||
className: PropTypes.string,
|
||||
identiconAddress: PropTypes.string,
|
||||
nonce: PropTypes.string,
|
||||
}
|
||||
|
||||
export default ConfirmPageContainerSummary
|
@ -0,0 +1 @@
|
||||
export { default } from './confirm-page-container-summary.component'
|
@ -0,0 +1,54 @@
|
||||
.confirm-page-container-summary {
|
||||
padding: 16px 24px 0;
|
||||
background-color: #f9fafa;
|
||||
height: 133px;
|
||||
box-sizing: border-box;
|
||||
|
||||
&__action-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
&__action {
|
||||
text-transform: uppercase;
|
||||
color: $oslo-gray;
|
||||
font-size: .75rem;
|
||||
padding: 3px 8px;
|
||||
border: 1px solid $oslo-gray;
|
||||
border-radius: 4px;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
&__nonce {
|
||||
color: $oslo-gray;
|
||||
}
|
||||
|
||||
&__title {
|
||||
padding: 4px 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
&__identicon {
|
||||
flex: 0 0 auto;
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
&__title-text {
|
||||
font-size: 2.25rem;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
&__subtitle {
|
||||
color: $oslo-gray;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
&--border {
|
||||
border-bottom: 1px solid $geyser;
|
||||
}
|
||||
}
|
@ -0,0 +1,22 @@
|
||||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
|
||||
const ConfirmPageContainerWarning = props => {
|
||||
return (
|
||||
<div className="confirm-page-container-warning">
|
||||
<img
|
||||
className="confirm-page-container-warning__icon"
|
||||
src="/images/alert.svg"
|
||||
/>
|
||||
<div className="confirm-page-container-warning__warning">
|
||||
{ props.warning }
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
ConfirmPageContainerWarning.propTypes = {
|
||||
warning: PropTypes.string,
|
||||
}
|
||||
|
||||
export default ConfirmPageContainerWarning
|
@ -0,0 +1 @@
|
||||
export { default } from './confirm-page-container-warning.component'
|
@ -0,0 +1,18 @@
|
||||
.confirm-page-container-warning {
|
||||
background-color: #fffcdb;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
border-bottom: 1px solid $geyser;
|
||||
padding: 12px 24px;
|
||||
|
||||
&__icon {
|
||||
flex: 0 0 auto;
|
||||
margin-right: 16px;
|
||||
}
|
||||
|
||||
&__warning {
|
||||
font-size: .75rem;
|
||||
color: #5f5922;
|
||||
}
|
||||
}
|
@ -0,0 +1,4 @@
|
||||
export { default } from './confirm-page-container-content.component'
|
||||
export { default as ConfirmPageContainerSummary } from './confirm-page-container-summary'
|
||||
export { default as ConfirmPageContainerError } from './confirm-page-container-error'
|
||||
export { default as ConfirmPageContainerWarning } from './confirm-page-container-warning'
|
@ -0,0 +1,66 @@
|
||||
@import './confirm-page-container-error/index';
|
||||
|
||||
@import './confirm-page-container-warning/index';
|
||||
|
||||
@import './confirm-page-container-summary/index';
|
||||
|
||||
.confirm-page-container-content {
|
||||
overflow-y: auto;
|
||||
flex: 1;
|
||||
|
||||
&__error-container {
|
||||
padding: 0 16px 16px 16px;
|
||||
}
|
||||
|
||||
&__details {
|
||||
box-sizing: border-box;
|
||||
padding: 0 24px;
|
||||
}
|
||||
|
||||
&__data {
|
||||
padding: 16px;
|
||||
color: $oslo-gray;
|
||||
}
|
||||
|
||||
&__data-box {
|
||||
background-color: #f9fafa;
|
||||
padding: 12px;
|
||||
font-size: .75rem;
|
||||
margin-bottom: 16px;
|
||||
word-wrap: break-word;
|
||||
max-height: 200px;
|
||||
overflow-y: auto;
|
||||
|
||||
&-label {
|
||||
text-transform: uppercase;
|
||||
padding: 8px 0 12px;
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
&__data-field {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
|
||||
&-label {
|
||||
font-weight: 500;
|
||||
padding-right: 16px;
|
||||
}
|
||||
|
||||
&:not(:last-child) {
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
}
|
||||
|
||||
&__gas-fee {
|
||||
border-bottom: 1px solid $geyser;
|
||||
}
|
||||
|
||||
&__function-type {
|
||||
font-size: .875rem;
|
||||
font-weight: 500;
|
||||
text-transform: capitalize;
|
||||
color: $black;
|
||||
padding-left: 5px;
|
||||
}
|
||||
}
|
@ -0,0 +1,63 @@
|
||||
import React, { Component } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import {
|
||||
ENVIRONMENT_TYPE_POPUP,
|
||||
ENVIRONMENT_TYPE_NOTIFICATION,
|
||||
} from '../../../../../app/scripts/lib/enums'
|
||||
import NetworkDisplay from '../../network-display'
|
||||
|
||||
export default class ConfirmPageContainer extends Component {
|
||||
static contextTypes = {
|
||||
t: PropTypes.func,
|
||||
}
|
||||
|
||||
static propTypes = {
|
||||
showEdit: PropTypes.bool,
|
||||
onEdit: PropTypes.func,
|
||||
children: PropTypes.node,
|
||||
}
|
||||
|
||||
renderTop () {
|
||||
const { onEdit, showEdit } = this.props
|
||||
const windowType = window.METAMASK_UI_TYPE
|
||||
const isFullScreen = windowType !== ENVIRONMENT_TYPE_NOTIFICATION &&
|
||||
windowType !== ENVIRONMENT_TYPE_POPUP
|
||||
|
||||
if (!showEdit && isFullScreen) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="confirm-page-container-header__row">
|
||||
<div
|
||||
className="confirm-page-container-header__back-button-container"
|
||||
style={{
|
||||
visibility: showEdit ? 'initial' : 'hidden',
|
||||
}}
|
||||
>
|
||||
<img
|
||||
src="/images/caret-left.svg"
|
||||
/>
|
||||
<span
|
||||
className="confirm-page-container-header__back-button"
|
||||
onClick={() => onEdit()}
|
||||
>
|
||||
{ this.context.t('edit') }
|
||||
</span>
|
||||
</div>
|
||||
{ !isFullScreen && <NetworkDisplay /> }
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
render () {
|
||||
const { children } = this.props
|
||||
|
||||
return (
|
||||
<div className="confirm-page-container-header">
|
||||
{ this.renderTop() }
|
||||
{ children }
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
@ -0,0 +1 @@
|
||||
export { default } from './confirm-page-container-header.component'
|
@ -0,0 +1,27 @@
|
||||
.confirm-page-container-header {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 0 0 auto;
|
||||
|
||||
&__row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
border-bottom: 1px solid $geyser;
|
||||
padding: 13px 13px 13px 24px;
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
&__back-button-container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
&__back-button {
|
||||
color: #2f9ae0;
|
||||
font-size: 1rem;
|
||||
cursor: pointer;
|
||||
font-weight: 400;
|
||||
padding-left: 5px;
|
||||
}
|
||||
}
|
@ -0,0 +1,118 @@
|
||||
import React, { Component } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import SenderToRecipient from '../sender-to-recipient'
|
||||
import { PageContainerFooter } from '../page-container'
|
||||
import { ConfirmPageContainerHeader, ConfirmPageContainerContent } from './'
|
||||
|
||||
export default class ConfirmPageContainer extends Component {
|
||||
static contextTypes = {
|
||||
t: PropTypes.func,
|
||||
}
|
||||
|
||||
static propTypes = {
|
||||
// Header
|
||||
action: PropTypes.string,
|
||||
hideSubtitle: PropTypes.bool,
|
||||
onEdit: PropTypes.func,
|
||||
showEdit: PropTypes.bool,
|
||||
subtitle: PropTypes.string,
|
||||
title: PropTypes.string,
|
||||
titleComponent: PropTypes.func,
|
||||
// Sender to Recipient
|
||||
fromAddress: PropTypes.string,
|
||||
fromName: PropTypes.string,
|
||||
toAddress: PropTypes.string,
|
||||
toName: PropTypes.string,
|
||||
// Content
|
||||
contentComponent: PropTypes.node,
|
||||
errorKey: PropTypes.string,
|
||||
errorMessage: PropTypes.string,
|
||||
fiatTransactionAmount: PropTypes.string,
|
||||
fiatTransactionFee: PropTypes.string,
|
||||
fiatTransactionTotal: PropTypes.string,
|
||||
ethTransactionAmount: PropTypes.string,
|
||||
ethTransactionFee: PropTypes.string,
|
||||
ethTransactionTotal: PropTypes.string,
|
||||
onEditGas: PropTypes.func,
|
||||
dataComponent: PropTypes.node,
|
||||
detailsComponent: PropTypes.node,
|
||||
identiconAddress: PropTypes.string,
|
||||
nonce: PropTypes.string,
|
||||
summaryComponent: PropTypes.node,
|
||||
warning: PropTypes.string,
|
||||
// Footer
|
||||
onCancel: PropTypes.func,
|
||||
onSubmit: PropTypes.func,
|
||||
valid: PropTypes.bool,
|
||||
}
|
||||
|
||||
render () {
|
||||
const {
|
||||
showEdit,
|
||||
onEdit,
|
||||
fromName,
|
||||
fromAddress,
|
||||
toName,
|
||||
toAddress,
|
||||
valid,
|
||||
errorKey,
|
||||
errorMessage,
|
||||
contentComponent,
|
||||
action,
|
||||
title,
|
||||
titleComponent,
|
||||
subtitle,
|
||||
hideSubtitle,
|
||||
summaryComponent,
|
||||
detailsComponent,
|
||||
dataComponent,
|
||||
onCancel,
|
||||
onSubmit,
|
||||
identiconAddress,
|
||||
nonce,
|
||||
warning,
|
||||
} = this.props
|
||||
|
||||
return (
|
||||
<div className="page-container">
|
||||
<ConfirmPageContainerHeader
|
||||
showEdit={showEdit}
|
||||
onEdit={() => onEdit()}
|
||||
>
|
||||
<SenderToRecipient
|
||||
senderName={fromName}
|
||||
senderAddress={fromAddress}
|
||||
recipientName={toName}
|
||||
recipientAddress={toAddress}
|
||||
/>
|
||||
</ConfirmPageContainerHeader>
|
||||
{
|
||||
contentComponent || (
|
||||
<ConfirmPageContainerContent
|
||||
action={action}
|
||||
title={title}
|
||||
titleComponent={titleComponent}
|
||||
subtitle={subtitle}
|
||||
hideSubtitle={hideSubtitle}
|
||||
summaryComponent={summaryComponent}
|
||||
detailsComponent={detailsComponent}
|
||||
dataComponent={dataComponent}
|
||||
errorMessage={errorMessage}
|
||||
errorKey={errorKey}
|
||||
identiconAddress={identiconAddress}
|
||||
nonce={nonce}
|
||||
warning={warning}
|
||||
/>
|
||||
)
|
||||
}
|
||||
<PageContainerFooter
|
||||
onCancel={() => onCancel()}
|
||||
onSubmit={() => onSubmit()}
|
||||
submitText={this.context.t('confirm')}
|
||||
submitButtonType="confirm"
|
||||
disabled={!valid}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
8
ui/app/components/confirm-page-container/index.js
Normal file
8
ui/app/components/confirm-page-container/index.js
Normal file
@ -0,0 +1,8 @@
|
||||
export { default } from './confirm-page-container.component'
|
||||
export { default as ConfirmPageContainerHeader } from './confirm-page-container-header'
|
||||
export { default as ConfirmDetailRow } from './confirm-detail-row'
|
||||
export {
|
||||
default as ConfirmPageContainerContent,
|
||||
ConfirmPageContainerSummary,
|
||||
ConfirmPageContainerError,
|
||||
} from './confirm-page-container-content'
|
5
ui/app/components/confirm-page-container/index.scss
Normal file
5
ui/app/components/confirm-page-container/index.scss
Normal file
@ -0,0 +1,5 @@
|
||||
@import './confirm-page-container-content/index';
|
||||
|
||||
@import './confirm-page-container-header/index';
|
||||
|
||||
@import './confirm-detail-row/index';
|
@ -15,6 +15,7 @@ NetworkDropdownIcon.prototype.render = function () {
|
||||
backgroundColor,
|
||||
isSelected,
|
||||
innerBorder = 'none',
|
||||
diameter = '12',
|
||||
} = this.props
|
||||
|
||||
return h(`.menu-icon-circle${isSelected ? '--active' : ''}`, {},
|
||||
@ -22,6 +23,8 @@ NetworkDropdownIcon.prototype.render = function () {
|
||||
style: {
|
||||
background: backgroundColor,
|
||||
border: innerBorder,
|
||||
height: `${diameter}px`,
|
||||
width: `${diameter}px`,
|
||||
},
|
||||
})
|
||||
)
|
||||
|
@ -4,6 +4,16 @@
|
||||
|
||||
@import './info-box/index';
|
||||
|
||||
@import './network-display/index';
|
||||
|
||||
@import './confirm-page-container/index';
|
||||
|
||||
@import './page-container/index';
|
||||
|
||||
@import './pages/index';
|
||||
|
||||
@import './modals/index';
|
||||
|
||||
@import './sender-to-recipient/index';
|
||||
|
||||
@import './tabs/index';
|
||||
|
@ -0,0 +1,140 @@
|
||||
import React, { Component } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import GasModalCard from '../../customize-gas-modal/gas-modal-card'
|
||||
import { MIN_GAS_PRICE_GWEI } from '../../send_/send.constants'
|
||||
|
||||
import {
|
||||
getDecimalGasLimit,
|
||||
getDecimalGasPrice,
|
||||
getPrefixedHexGasLimit,
|
||||
getPrefixedHexGasPrice,
|
||||
} from './customize-gas.util'
|
||||
|
||||
export default class CustomizeGas extends Component {
|
||||
static contextTypes = {
|
||||
t: PropTypes.func,
|
||||
}
|
||||
|
||||
static propTypes = {
|
||||
txData: PropTypes.object.isRequired,
|
||||
hideModal: PropTypes.func,
|
||||
validate: PropTypes.func,
|
||||
onSubmit: PropTypes.func,
|
||||
}
|
||||
|
||||
state = {
|
||||
gasPrice: 0,
|
||||
gasLimit: 0,
|
||||
originalGasPrice: 0,
|
||||
originalGasLimit: 0,
|
||||
}
|
||||
|
||||
componentDidMount () {
|
||||
const { txData = {} } = this.props
|
||||
const { txParams: { gas: hexGasLimit, gasPrice: hexGasPrice } = {} } = txData
|
||||
|
||||
const gasLimit = getDecimalGasLimit(hexGasLimit)
|
||||
const gasPrice = getDecimalGasPrice(hexGasPrice)
|
||||
|
||||
this.setState({
|
||||
gasPrice,
|
||||
gasLimit,
|
||||
originalGasPrice: gasPrice,
|
||||
originalGasLimit: gasLimit,
|
||||
})
|
||||
}
|
||||
|
||||
handleRevert () {
|
||||
const { originalGasPrice, originalGasLimit } = this.state
|
||||
|
||||
this.setState({
|
||||
gasPrice: originalGasPrice,
|
||||
gasLimit: originalGasLimit,
|
||||
})
|
||||
}
|
||||
|
||||
handleSave () {
|
||||
const { onSubmit, hideModal } = this.props
|
||||
const { gasLimit, gasPrice } = this.state
|
||||
const prefixedHexGasPrice = getPrefixedHexGasPrice(gasPrice)
|
||||
const prefixedHexGasLimit = getPrefixedHexGasLimit(gasLimit)
|
||||
|
||||
Promise.resolve(onSubmit({ gasPrice: prefixedHexGasPrice, gasLimit: prefixedHexGasLimit }))
|
||||
.then(() => hideModal())
|
||||
}
|
||||
|
||||
validate () {
|
||||
const { gasLimit, gasPrice } = this.state
|
||||
return this.props.validate({
|
||||
gasPrice: getPrefixedHexGasPrice(gasPrice),
|
||||
gasLimit: getPrefixedHexGasLimit(gasLimit),
|
||||
})
|
||||
}
|
||||
|
||||
render () {
|
||||
const { t } = this.context
|
||||
const { hideModal } = this.props
|
||||
const { gasPrice, gasLimit } = this.state
|
||||
const { valid, errorKey } = this.validate()
|
||||
|
||||
return (
|
||||
<div className="customize-gas">
|
||||
<div className="customize-gas__content">
|
||||
<div className="customize-gas__header">
|
||||
<div className="customize-gas__title">
|
||||
{ this.context.t('customGas') }
|
||||
</div>
|
||||
<div
|
||||
className="customize-gas__close"
|
||||
onClick={() => hideModal()}
|
||||
/>
|
||||
</div>
|
||||
<div className="customize-gas__body">
|
||||
<GasModalCard
|
||||
value={gasPrice}
|
||||
min={MIN_GAS_PRICE_GWEI}
|
||||
step={1}
|
||||
onChange={value => this.setState({ gasPrice: value })}
|
||||
title={t('gasPrice')}
|
||||
copy={t('gasPriceCalculation')}
|
||||
/>
|
||||
<GasModalCard
|
||||
value={gasLimit}
|
||||
min={1}
|
||||
step={1}
|
||||
onChange={value => this.setState({ gasLimit: value })}
|
||||
title={t('gasLimit')}
|
||||
copy={t('gasLimitCalculation')}
|
||||
/>
|
||||
</div>
|
||||
<div className="customize-gas__footer">
|
||||
{ !valid && <div className="customize-gas__error-message">{ t(errorKey) }</div> }
|
||||
<div
|
||||
className="customize-gas__revert"
|
||||
onClick={() => this.handleRevert()}
|
||||
>
|
||||
{ t('revert') }
|
||||
</div>
|
||||
<div className="customize-gas__buttons">
|
||||
<button
|
||||
className="btn-default customize-gas__cancel"
|
||||
onClick={() => hideModal()}
|
||||
style={{ marginRight: '10px' }}
|
||||
>
|
||||
{ t('cancel') }
|
||||
</button>
|
||||
<button
|
||||
className="btn-primary customize-gas__save"
|
||||
onClick={() => this.handleSave()}
|
||||
style={{ marginRight: '10px' }}
|
||||
disabled={!valid}
|
||||
>
|
||||
{ t('save') }
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
@ -0,0 +1,22 @@
|
||||
import { connect } from 'react-redux'
|
||||
import CustomizeGas from './customize-gas.component'
|
||||
import { hideModal } from '../../../actions'
|
||||
|
||||
const mapStateToProps = state => {
|
||||
const { appState: { modal: { modalState: { props } } } } = state
|
||||
const { txData, onSubmit, validate } = props
|
||||
|
||||
return {
|
||||
txData,
|
||||
onSubmit,
|
||||
validate,
|
||||
}
|
||||
}
|
||||
|
||||
const mapDispatchToProps = dispatch => {
|
||||
return {
|
||||
hideModal: () => dispatch(hideModal()),
|
||||
}
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(CustomizeGas)
|
34
ui/app/components/modals/customize-gas/customize-gas.util.js
Normal file
34
ui/app/components/modals/customize-gas/customize-gas.util.js
Normal file
@ -0,0 +1,34 @@
|
||||
import ethUtil from 'ethereumjs-util'
|
||||
import { conversionUtil } from '../../../conversion-util'
|
||||
|
||||
export function getDecimalGasLimit (hexGasLimit) {
|
||||
return conversionUtil(hexGasLimit, {
|
||||
fromNumericBase: 'hex',
|
||||
toNumericBase: 'dec',
|
||||
})
|
||||
}
|
||||
|
||||
export function getDecimalGasPrice (hexGasPrice) {
|
||||
return conversionUtil(hexGasPrice, {
|
||||
fromNumericBase: 'hex',
|
||||
toNumericBase: 'dec',
|
||||
fromDenomination: 'WEI',
|
||||
toDenomination: 'GWEI',
|
||||
})
|
||||
}
|
||||
|
||||
export function getPrefixedHexGasLimit (gasLimit) {
|
||||
return ethUtil.addHexPrefix(conversionUtil(gasLimit, {
|
||||
fromNumericBase: 'dec',
|
||||
toNumericBase: 'hex',
|
||||
}))
|
||||
}
|
||||
|
||||
export function getPrefixedHexGasPrice (gasPrice) {
|
||||
return ethUtil.addHexPrefix(conversionUtil(gasPrice, {
|
||||
fromNumericBase: 'dec',
|
||||
toNumericBase: 'hex',
|
||||
fromDenomination: 'GWEI',
|
||||
toDenomination: 'WEI',
|
||||
}))
|
||||
}
|
1
ui/app/components/modals/customize-gas/index.js
Normal file
1
ui/app/components/modals/customize-gas/index.js
Normal file
@ -0,0 +1 @@
|
||||
export { default } from './customize-gas.container'
|
110
ui/app/components/modals/customize-gas/index.scss
Normal file
110
ui/app/components/modals/customize-gas/index.scss
Normal file
@ -0,0 +1,110 @@
|
||||
.customize-gas {
|
||||
border: 1px solid #D8D8D8;
|
||||
border-radius: 4px;
|
||||
background-color: #FFFFFF;
|
||||
box-shadow: 0 2px 4px 0 rgba(0,0,0,0.14);
|
||||
font-family: Roboto;
|
||||
display: flex;
|
||||
flex-flow: column;
|
||||
|
||||
@media screen and (max-width: $break-small) {
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
}
|
||||
|
||||
&__header {
|
||||
height: 52px;
|
||||
border-bottom: 1px solid $alto;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
font-size: 22px;
|
||||
|
||||
@media screen and (max-width: $break-small) {
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
}
|
||||
|
||||
&__title {
|
||||
margin-left: 19.25px;
|
||||
}
|
||||
|
||||
&__close::after {
|
||||
content: '\00D7';
|
||||
font-size: 1.8em;
|
||||
color: $dusty-gray;
|
||||
font-family: sans-serif;
|
||||
cursor: pointer;
|
||||
margin-right: 19.25px;
|
||||
}
|
||||
|
||||
&__content {
|
||||
display: flex;
|
||||
flex-flow: column nowrap;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
&__body {
|
||||
display: flex;
|
||||
margin-bottom: 24px;
|
||||
|
||||
@media screen and (max-width: $break-small) {
|
||||
flex-flow: column;
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
}
|
||||
|
||||
&__footer {
|
||||
height: 75px;
|
||||
border-top: 1px solid $alto;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
font-size: 22px;
|
||||
position: relative;
|
||||
|
||||
@media screen and (max-width: $break-small) {
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
}
|
||||
|
||||
&__buttons {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin-right: 21.25px;
|
||||
}
|
||||
|
||||
&__revert, &__cancel, &__save, &__save__error {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
padding: 0 3px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
&__revert {
|
||||
color: $silver-chalice;
|
||||
font-size: 16px;
|
||||
margin-left: 21.25px;
|
||||
}
|
||||
|
||||
&__cancel, &__save, &__save__error {
|
||||
width: 85.74px;
|
||||
min-width: initial;
|
||||
}
|
||||
|
||||
&__save__error {
|
||||
opacity: 0.5;
|
||||
cursor: auto;
|
||||
}
|
||||
|
||||
&__error-message {
|
||||
display: block;
|
||||
position: absolute;
|
||||
top: 4px;
|
||||
right: 4px;
|
||||
font-size: 12px;
|
||||
line-height: 12px;
|
||||
color: $red;
|
||||
}
|
||||
}
|
@ -1,3 +1,5 @@
|
||||
@import './customize-gas/index';
|
||||
|
||||
.modal-container {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
@ -25,6 +25,8 @@ const TransactionConfirmed = require('./transaction-confirmed')
|
||||
const WelcomeBeta = require('./welcome-beta')
|
||||
const Notification = require('./notification')
|
||||
|
||||
import ConfirmCustomizeGasModal from './customize-gas'
|
||||
|
||||
const modalContainerBaseStyle = {
|
||||
transform: 'translate3d(-50%, 0, 0px)',
|
||||
border: '1px solid #CCCFD1',
|
||||
@ -281,7 +283,31 @@ const MODALS = {
|
||||
|
||||
CUSTOMIZE_GAS: {
|
||||
contents: [
|
||||
h(CustomizeGasModal, {}, []),
|
||||
h(CustomizeGasModal),
|
||||
],
|
||||
mobileModalStyle: {
|
||||
width: '100vw',
|
||||
height: '100vh',
|
||||
top: '0',
|
||||
transform: 'none',
|
||||
left: '0',
|
||||
right: '0',
|
||||
margin: '0 auto',
|
||||
},
|
||||
laptopModalStyle: {
|
||||
width: '720px',
|
||||
height: '377px',
|
||||
top: '80px',
|
||||
transform: 'none',
|
||||
left: '0',
|
||||
right: '0',
|
||||
margin: '0 auto',
|
||||
},
|
||||
},
|
||||
|
||||
CONFIRM_CUSTOMIZE_GAS: {
|
||||
contents: [
|
||||
h(ConfirmCustomizeGasModal),
|
||||
],
|
||||
mobileModalStyle: {
|
||||
width: '100vw',
|
||||
|
@ -1,56 +0,0 @@
|
||||
const { Component } = require('react')
|
||||
const h = require('react-hyperscript')
|
||||
const PropTypes = require('prop-types')
|
||||
const connect = require('react-redux').connect
|
||||
const NetworkDropdownIcon = require('./dropdowns/components/network-dropdown-icon')
|
||||
|
||||
const networkToColorHash = {
|
||||
1: '#038789',
|
||||
3: '#e91550',
|
||||
42: '#690496',
|
||||
4: '#ebb33f',
|
||||
}
|
||||
|
||||
class NetworkDisplay extends Component {
|
||||
renderNetworkIcon () {
|
||||
const { network } = this.props
|
||||
const networkColor = networkToColorHash[network]
|
||||
|
||||
return networkColor
|
||||
? h(NetworkDropdownIcon, { backgroundColor: networkColor })
|
||||
: h('i.fa.fa-question-circle.fa-med', {
|
||||
style: {
|
||||
margin: '0 4px',
|
||||
color: 'rgb(125, 128, 130)',
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
render () {
|
||||
const { provider: { type } } = this.props
|
||||
return h('.network-display__container', [
|
||||
this.renderNetworkIcon(),
|
||||
h('.network-name', this.context.t(type)),
|
||||
])
|
||||
}
|
||||
}
|
||||
|
||||
NetworkDisplay.propTypes = {
|
||||
network: PropTypes.string,
|
||||
provider: PropTypes.object,
|
||||
t: PropTypes.func,
|
||||
}
|
||||
|
||||
const mapStateToProps = ({ metamask: { network, provider } }) => {
|
||||
return {
|
||||
network,
|
||||
provider,
|
||||
}
|
||||
}
|
||||
|
||||
NetworkDisplay.contextTypes = {
|
||||
t: PropTypes.func,
|
||||
}
|
||||
|
||||
module.exports = connect(mapStateToProps)(NetworkDisplay)
|
||||
|
2
ui/app/components/network-display/index.js
Normal file
2
ui/app/components/network-display/index.js
Normal file
@ -0,0 +1,2 @@
|
||||
import NetworkDisplay from './network-display.container'
|
||||
module.exports = NetworkDisplay
|
54
ui/app/components/network-display/index.scss
Normal file
54
ui/app/components/network-display/index.scss
Normal file
@ -0,0 +1,54 @@
|
||||
.network-display {
|
||||
&__container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
background-color: lighten(rgb(125, 128, 130), 45%);
|
||||
padding: 0 10px;
|
||||
border-radius: 4px;
|
||||
height: 25px;
|
||||
|
||||
&--mainnet {
|
||||
background-color: lighten($blue-lagoon, 45%);
|
||||
}
|
||||
|
||||
&--ropsten {
|
||||
background-color: lighten($crimson, 45%);
|
||||
}
|
||||
|
||||
&--kovan {
|
||||
background-color: lighten($purple, 45%);
|
||||
}
|
||||
|
||||
&--rinkeby {
|
||||
background-color: lighten($tulip-tree, 45%);
|
||||
}
|
||||
}
|
||||
|
||||
&__name {
|
||||
font-size: .75rem;
|
||||
padding-left: 5px;
|
||||
}
|
||||
|
||||
&__icon {
|
||||
height: 10px;
|
||||
width: 10px;
|
||||
border-radius: 10px;
|
||||
|
||||
&--mainnet {
|
||||
background-color: $blue-lagoon;
|
||||
}
|
||||
|
||||
&--ropsten {
|
||||
background-color: $crimson;
|
||||
}
|
||||
|
||||
&--kovan {
|
||||
background-color: $purple;
|
||||
}
|
||||
|
||||
&--rinkeby {
|
||||
background-color: $tulip-tree;
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,69 @@
|
||||
import React, { Component } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import classnames from 'classnames'
|
||||
import {
|
||||
MAINNET_CODE,
|
||||
ROPSTEN_CODE,
|
||||
RINKEYBY_CODE,
|
||||
KOVAN_CODE,
|
||||
} from '../../../../app/scripts/controllers/network/enums'
|
||||
|
||||
const networkToClassHash = {
|
||||
[MAINNET_CODE]: 'mainnet',
|
||||
[ROPSTEN_CODE]: 'ropsten',
|
||||
[RINKEYBY_CODE]: 'rinkeby',
|
||||
[KOVAN_CODE]: 'kovan',
|
||||
}
|
||||
|
||||
export default class NetworkDisplay extends Component {
|
||||
static propTypes = {
|
||||
network: PropTypes.string,
|
||||
provider: PropTypes.object,
|
||||
}
|
||||
|
||||
static contextTypes = {
|
||||
t: PropTypes.func,
|
||||
}
|
||||
|
||||
renderNetworkIcon () {
|
||||
const { network } = this.props
|
||||
const networkClass = networkToClassHash[network]
|
||||
|
||||
return networkClass
|
||||
? <div className={`network-display__icon network-display__icon--${networkClass}`} />
|
||||
: <div
|
||||
className="i fa fa-question-circle fa-med"
|
||||
style={{
|
||||
margin: '0 4px',
|
||||
color: 'rgb(125, 128, 130)',
|
||||
}}
|
||||
/>
|
||||
}
|
||||
|
||||
render () {
|
||||
const { network, provider: { type } } = this.props
|
||||
const networkClass = networkToClassHash[network]
|
||||
|
||||
return (
|
||||
<div className={classnames(
|
||||
'network-display__container',
|
||||
networkClass && ('network-display__container--' + networkClass)
|
||||
)}>
|
||||
{
|
||||
networkClass
|
||||
? <div className={`network-display__icon network-display__icon--${networkClass}`} />
|
||||
: <div
|
||||
className="i fa fa-question-circle fa-med"
|
||||
style={{
|
||||
margin: '0 4px',
|
||||
color: 'rgb(125, 128, 130)',
|
||||
}}
|
||||
/>
|
||||
}
|
||||
<div className="network-display__name">
|
||||
{ this.context.t(type) }
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
@ -0,0 +1,11 @@
|
||||
import { connect } from 'react-redux'
|
||||
import NetworkDisplay from './network-display.component'
|
||||
|
||||
const mapStateToProps = ({ metamask: { network, provider } }) => {
|
||||
return {
|
||||
network,
|
||||
provider,
|
||||
}
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps)(NetworkDisplay)
|
@ -1 +1,4 @@
|
||||
import PageContainerHeader from './page-container-header'
|
||||
import PageContainerFooter from './page-container-footer'
|
||||
export { default } from './page-container.component'
|
||||
export { PageContainerHeader, PageContainerFooter }
|
||||
|
186
ui/app/components/page-container/index.scss
Normal file
186
ui/app/components/page-container/index.scss
Normal file
@ -0,0 +1,186 @@
|
||||
.page-container {
|
||||
width: 408px;
|
||||
background-color: $white;
|
||||
box-shadow: 0 0 7px 0 rgba(0, 0, 0, .08);
|
||||
z-index: 25;
|
||||
display: flex;
|
||||
flex-flow: column;
|
||||
border-radius: 8px;
|
||||
|
||||
&__header {
|
||||
display: flex;
|
||||
flex-flow: column;
|
||||
border-bottom: 1px solid $geyser;
|
||||
padding: 16px;
|
||||
flex: 0 0 auto;
|
||||
position: relative;
|
||||
|
||||
&--no-padding-bottom {
|
||||
padding-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
&__header-close {
|
||||
color: $tundora;
|
||||
position: absolute;
|
||||
top: 16px;
|
||||
right: 16px;
|
||||
cursor: pointer;
|
||||
overflow: hidden;
|
||||
|
||||
&::after {
|
||||
content: '\00D7';
|
||||
font-size: 40px;
|
||||
line-height: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
&__header-row {
|
||||
padding-bottom: 10px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
&__footer {
|
||||
display: flex;
|
||||
flex-flow: row;
|
||||
justify-content: center;
|
||||
border-top: 1px solid $geyser;
|
||||
padding: 16px;
|
||||
flex: 0 0 auto;
|
||||
|
||||
.btn-default,
|
||||
.btn-confirm {
|
||||
font-size: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
&__footer-button {
|
||||
height: 55px;
|
||||
font-size: 1rem;
|
||||
text-transform: uppercase;
|
||||
margin-right: 16px;
|
||||
|
||||
&:last-of-type {
|
||||
margin-right: 0;
|
||||
}
|
||||
}
|
||||
|
||||
&__back-button {
|
||||
color: #2f9ae0;
|
||||
font-size: 1rem;
|
||||
cursor: pointer;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
&__title {
|
||||
color: $black;
|
||||
font-size: 2rem;
|
||||
font-weight: 500;
|
||||
line-height: 2rem;
|
||||
}
|
||||
|
||||
&__subtitle {
|
||||
padding-top: .5rem;
|
||||
line-height: initial;
|
||||
font-size: .9rem;
|
||||
color: $gray;
|
||||
}
|
||||
|
||||
&__tabs {
|
||||
display: flex;
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
&__tab {
|
||||
min-width: 5rem;
|
||||
padding: 8px;
|
||||
color: $dusty-gray;
|
||||
font-family: Roboto;
|
||||
font-size: 1rem;
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
border-bottom: none;
|
||||
margin-right: 16px;
|
||||
|
||||
&:last-of-type {
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
&--selected {
|
||||
color: $curious-blue;
|
||||
border-bottom: 3px solid $curious-blue;
|
||||
}
|
||||
}
|
||||
|
||||
&--full-width {
|
||||
width: 100% !important;
|
||||
}
|
||||
|
||||
&--full-height {
|
||||
height: 100% !important;
|
||||
max-height: initial !important;
|
||||
min-height: initial !important;
|
||||
}
|
||||
|
||||
&__content {
|
||||
overflow-y: auto;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
&__warning-container {
|
||||
background: $linen;
|
||||
padding: 20px;
|
||||
display: flex;
|
||||
align-items: start;
|
||||
}
|
||||
|
||||
&__warning-message {
|
||||
padding-left: 15px;
|
||||
}
|
||||
|
||||
&__warning-title {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
&__warning-icon {
|
||||
padding-top: 5px;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 250px) {
|
||||
.page-container {
|
||||
&__footer {
|
||||
flex-flow: column-reverse;
|
||||
}
|
||||
|
||||
&__footer-button {
|
||||
width: 100%;
|
||||
margin-bottom: 1rem;
|
||||
margin-right: 0;
|
||||
|
||||
&:first-of-type {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 575px) {
|
||||
.page-container {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
overflow-y: auto;
|
||||
background-color: $white;
|
||||
border-radius: 0;
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (min-width: 576px) {
|
||||
.page-container {
|
||||
max-height: 82vh;
|
||||
min-height: 570px;
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
}
|
@ -10,6 +10,7 @@ export default class PageContainerFooter extends Component {
|
||||
onSubmit: PropTypes.func,
|
||||
submitText: PropTypes.string,
|
||||
disabled: PropTypes.bool,
|
||||
submitButtonType: PropTypes.string,
|
||||
}
|
||||
|
||||
static contextTypes = {
|
||||
@ -23,6 +24,7 @@ export default class PageContainerFooter extends Component {
|
||||
onSubmit,
|
||||
submitText,
|
||||
disabled,
|
||||
submitButtonType,
|
||||
} = this.props
|
||||
|
||||
return (
|
||||
@ -30,16 +32,16 @@ export default class PageContainerFooter extends Component {
|
||||
|
||||
<Button
|
||||
type="default"
|
||||
large={true}
|
||||
large
|
||||
className="page-container__footer-button"
|
||||
onClick={() => onCancel()}
|
||||
onClick={e => onCancel(e)}
|
||||
>
|
||||
{ cancelText || this.context.t('cancel') }
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
type="primary"
|
||||
large={true}
|
||||
type={submitButtonType || 'primary'}
|
||||
large
|
||||
className="page-container__footer-button"
|
||||
disabled={disabled}
|
||||
onClick={e => onSubmit(e)}
|
||||
|
@ -1,35 +0,0 @@
|
||||
import React, { Component } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
|
||||
export default class PageContainerHeader extends Component {
|
||||
|
||||
static propTypes = {
|
||||
title: PropTypes.string,
|
||||
subtitle: PropTypes.string,
|
||||
onClose: PropTypes.func,
|
||||
};
|
||||
|
||||
render () {
|
||||
const { title, subtitle, onClose } = this.props
|
||||
|
||||
return (
|
||||
<div className="page-container__header">
|
||||
|
||||
<div className="page-container__title">
|
||||
{title}
|
||||
</div>
|
||||
|
||||
<div className="page-container__subtitle">
|
||||
{subtitle}
|
||||
</div>
|
||||
|
||||
<div
|
||||
className="page-container__header-close"
|
||||
onClick={() => onClose()}
|
||||
/>
|
||||
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
}
|
@ -4,13 +4,14 @@ import PropTypes from 'prop-types'
|
||||
export default class PageContainerHeader extends Component {
|
||||
|
||||
static propTypes = {
|
||||
title: PropTypes.string.isRequired,
|
||||
title: PropTypes.string,
|
||||
subtitle: PropTypes.string,
|
||||
onClose: PropTypes.func,
|
||||
showBackButton: PropTypes.bool,
|
||||
onBackButtonClick: PropTypes.func,
|
||||
backButtonStyles: PropTypes.object,
|
||||
backButtonString: PropTypes.string,
|
||||
children: PropTypes.node,
|
||||
};
|
||||
|
||||
renderHeaderRow () {
|
||||
@ -30,25 +31,33 @@ export default class PageContainerHeader extends Component {
|
||||
}
|
||||
|
||||
render () {
|
||||
const { title, subtitle, onClose } = this.props
|
||||
const { title, subtitle, onClose, children } = this.props
|
||||
|
||||
return (
|
||||
<div className="page-container__header">
|
||||
|
||||
{ this.renderHeaderRow() }
|
||||
|
||||
<div className="page-container__title">
|
||||
{title}
|
||||
</div>
|
||||
{ children }
|
||||
|
||||
<div className="page-container__subtitle">
|
||||
{subtitle}
|
||||
</div>
|
||||
{
|
||||
title && <div className="page-container__title">
|
||||
{ title }
|
||||
</div>
|
||||
}
|
||||
|
||||
<div
|
||||
className="page-container__header-close"
|
||||
onClick={() => onClose()}
|
||||
/>
|
||||
{
|
||||
subtitle && <div className="page-container__subtitle">
|
||||
{ subtitle }
|
||||
</div>
|
||||
}
|
||||
|
||||
{
|
||||
onClose && <div
|
||||
className="page-container__header-close"
|
||||
onClick={() => onClose()}
|
||||
/>
|
||||
}
|
||||
|
||||
</div>
|
||||
)
|
||||
|
@ -0,0 +1,30 @@
|
||||
import React, { Component } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import ConfirmTransactionBase from '../confirm-transaction-base'
|
||||
|
||||
export default class ConfirmApprove extends Component {
|
||||
static contextTypes = {
|
||||
t: PropTypes.func,
|
||||
}
|
||||
|
||||
static propTypes = {
|
||||
tokenAddress: PropTypes.string,
|
||||
toAddress: PropTypes.string,
|
||||
tokenAmount: PropTypes.string,
|
||||
tokenSymbol: PropTypes.string,
|
||||
}
|
||||
|
||||
render () {
|
||||
const { toAddress, tokenAddress, tokenAmount, tokenSymbol } = this.props
|
||||
|
||||
return (
|
||||
<ConfirmTransactionBase
|
||||
toAddress={toAddress}
|
||||
identiconAddress={tokenAddress}
|
||||
title={`${tokenAmount} ${tokenSymbol}`}
|
||||
warning={`By approving this action, you grant permission for this contract to spend up to ${tokenAmount} of your ${tokenSymbol}.`}
|
||||
hideSubtitle
|
||||
/>
|
||||
)
|
||||
}
|
||||
}
|
@ -0,0 +1,28 @@
|
||||
import { connect } from 'react-redux'
|
||||
import ConfirmApprove from './confirm-approve.component'
|
||||
|
||||
const mapStateToProps = state => {
|
||||
const { confirmTransaction } = state
|
||||
const {
|
||||
tokenData = {},
|
||||
txData: { txParams: { to: tokenAddress } = {} } = {},
|
||||
tokenProps: { tokenSymbol } = {},
|
||||
} = confirmTransaction
|
||||
const { params = [] } = tokenData
|
||||
|
||||
let toAddress = ''
|
||||
let tokenAmount = ''
|
||||
|
||||
if (params && params.length === 2) {
|
||||
[{ value: toAddress }, { value: tokenAmount }] = params
|
||||
}
|
||||
|
||||
return {
|
||||
toAddress,
|
||||
tokenAddress,
|
||||
tokenAmount,
|
||||
tokenSymbol,
|
||||
}
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps)(ConfirmApprove)
|
1
ui/app/components/pages/confirm-approve/index.js
Normal file
1
ui/app/components/pages/confirm-approve/index.js
Normal file
@ -0,0 +1 @@
|
||||
export { default } from './confirm-approve.container'
|
@ -0,0 +1,64 @@
|
||||
import React, { Component } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import ethUtil from 'ethereumjs-util'
|
||||
import ConfirmTransactionBase from '../confirm-transaction-base'
|
||||
|
||||
export default class ConfirmDeployContract extends Component {
|
||||
static contextTypes = {
|
||||
t: PropTypes.func,
|
||||
}
|
||||
|
||||
static propTypes = {
|
||||
txData: PropTypes.object,
|
||||
}
|
||||
|
||||
renderData () {
|
||||
const { t } = this.context
|
||||
const {
|
||||
txData: {
|
||||
origin,
|
||||
txParams: {
|
||||
data,
|
||||
} = {},
|
||||
} = {},
|
||||
} = this.props
|
||||
|
||||
return (
|
||||
<div className="confirm-page-container-content__data">
|
||||
<div className="confirm-page-container-content__data-box">
|
||||
<div className="confirm-page-container-content__data-field">
|
||||
<div className="confirm-page-container-content__data-field-label">
|
||||
{ `${t('origin')}:` }
|
||||
</div>
|
||||
<div>
|
||||
{ origin }
|
||||
</div>
|
||||
</div>
|
||||
<div className="confirm-page-container-content__data-field">
|
||||
<div className="confirm-page-container-content__data-field-label">
|
||||
{ `${t('bytes')}:` }
|
||||
</div>
|
||||
<div>
|
||||
{ ethUtil.toBuffer(data).length }
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="confirm-page-container-content__data-box-label">
|
||||
{ `${t('hexData')}:` }
|
||||
</div>
|
||||
<div className="confirm-page-container-content__data-box">
|
||||
{ data }
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
render () {
|
||||
return (
|
||||
<ConfirmTransactionBase
|
||||
action={this.context.t('contractDeployment')}
|
||||
dataComponent={this.renderData()}
|
||||
/>
|
||||
)
|
||||
}
|
||||
}
|
@ -0,0 +1,12 @@
|
||||
import { connect } from 'react-redux'
|
||||
import ConfirmDeployContract from './confirm-deploy-contract.component'
|
||||
|
||||
const mapStateToProps = state => {
|
||||
const { confirmTransaction: { txData } = {} } = state
|
||||
|
||||
return {
|
||||
txData,
|
||||
}
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps)(ConfirmDeployContract)
|
1
ui/app/components/pages/confirm-deploy-contract/index.js
Normal file
1
ui/app/components/pages/confirm-deploy-contract/index.js
Normal file
@ -0,0 +1 @@
|
||||
export { default } from './confirm-deploy-contract.container'
|
@ -0,0 +1,39 @@
|
||||
import React, { Component } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import ConfirmTransactionBase from '../confirm-transaction-base'
|
||||
import { SEND_ROUTE } from '../../../routes'
|
||||
|
||||
export default class ConfirmSendEther extends Component {
|
||||
static contextTypes = {
|
||||
t: PropTypes.func,
|
||||
}
|
||||
|
||||
static propTypes = {
|
||||
editTransaction: PropTypes.func,
|
||||
history: PropTypes.object,
|
||||
txParams: PropTypes.object,
|
||||
}
|
||||
|
||||
handleEdit ({ txData }) {
|
||||
const { editTransaction, history } = this.props
|
||||
editTransaction(txData)
|
||||
history.push(SEND_ROUTE)
|
||||
}
|
||||
|
||||
shouldHideData () {
|
||||
const { txParams = {} } = this.props
|
||||
return !txParams.data
|
||||
}
|
||||
|
||||
render () {
|
||||
const hideData = this.shouldHideData()
|
||||
|
||||
return (
|
||||
<ConfirmTransactionBase
|
||||
action={this.context.t('confirm')}
|
||||
hideData={hideData}
|
||||
onEdit={confirmTransactionData => this.handleEdit(confirmTransactionData)}
|
||||
/>
|
||||
)
|
||||
}
|
||||
}
|
@ -0,0 +1,45 @@
|
||||
import { connect } from 'react-redux'
|
||||
import { compose } from 'recompose'
|
||||
import { withRouter } from 'react-router-dom'
|
||||
import { updateSend } from '../../../actions'
|
||||
import { clearConfirmTransaction } from '../../../ducks/confirm-transaction.duck'
|
||||
import ConfirmSendEther from './confirm-send-ether.component'
|
||||
|
||||
const mapStateToProps = state => {
|
||||
const { confirmTransaction: { txData: { txParams } = {} } } = state
|
||||
|
||||
return {
|
||||
txParams,
|
||||
}
|
||||
}
|
||||
|
||||
const mapDispatchToProps = dispatch => {
|
||||
return {
|
||||
editTransaction: txData => {
|
||||
const { id, txParams } = txData
|
||||
const {
|
||||
gas: gasLimit,
|
||||
gasPrice,
|
||||
to,
|
||||
value: amount,
|
||||
} = txParams
|
||||
|
||||
dispatch(updateSend({
|
||||
gasLimit,
|
||||
gasPrice,
|
||||
gasTotal: null,
|
||||
to,
|
||||
amount,
|
||||
errors: { to: null, amount: null },
|
||||
editingTransactionId: id && id.toString(),
|
||||
}))
|
||||
|
||||
dispatch(clearConfirmTransaction())
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
export default compose(
|
||||
withRouter,
|
||||
connect(mapStateToProps, mapDispatchToProps)
|
||||
)(ConfirmSendEther)
|
1
ui/app/components/pages/confirm-send-ether/index.js
Normal file
1
ui/app/components/pages/confirm-send-ether/index.js
Normal file
@ -0,0 +1 @@
|
||||
export { default } from './confirm-send-ether.container'
|
@ -0,0 +1,39 @@
|
||||
import React, { Component } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import ConfirmTransactionBase from '../confirm-transaction-base'
|
||||
import { SEND_ROUTE } from '../../../routes'
|
||||
|
||||
export default class ConfirmSendToken extends Component {
|
||||
static contextTypes = {
|
||||
t: PropTypes.func,
|
||||
}
|
||||
|
||||
static propTypes = {
|
||||
history: PropTypes.object,
|
||||
tokenAddress: PropTypes.string,
|
||||
toAddress: PropTypes.string,
|
||||
numberOfTokens: PropTypes.number,
|
||||
tokenSymbol: PropTypes.string,
|
||||
editTransaction: PropTypes.func,
|
||||
}
|
||||
|
||||
handleEdit (confirmTransactionData) {
|
||||
const { editTransaction, history } = this.props
|
||||
editTransaction(confirmTransactionData)
|
||||
history.push(SEND_ROUTE)
|
||||
}
|
||||
|
||||
render () {
|
||||
const { toAddress, tokenAddress, tokenSymbol, numberOfTokens } = this.props
|
||||
|
||||
return (
|
||||
<ConfirmTransactionBase
|
||||
toAddress={toAddress}
|
||||
identiconAddress={tokenAddress}
|
||||
title={`${numberOfTokens} ${tokenSymbol}`}
|
||||
onEdit={confirmTransactionData => this.handleEdit(confirmTransactionData)}
|
||||
hideSubtitle
|
||||
/>
|
||||
)
|
||||
}
|
||||
}
|
@ -0,0 +1,72 @@
|
||||
import { connect } from 'react-redux'
|
||||
import { compose } from 'recompose'
|
||||
import { withRouter } from 'react-router-dom'
|
||||
import ConfirmSendToken from './confirm-send-token.component'
|
||||
import { calcTokenAmount } from '../../../token-util'
|
||||
import { clearConfirmTransaction } from '../../../ducks/confirm-transaction.duck'
|
||||
import { setSelectedToken, updateSend, showSendTokenPage } from '../../../actions'
|
||||
import { conversionUtil } from '../../../conversion-util'
|
||||
|
||||
const mapStateToProps = state => {
|
||||
const { confirmTransaction } = state
|
||||
const {
|
||||
tokenData = {},
|
||||
tokenProps: { tokenSymbol, tokenDecimals } = {},
|
||||
txData: { txParams: { to: tokenAddress } = {} } = {},
|
||||
} = confirmTransaction
|
||||
const { params = [] } = tokenData
|
||||
|
||||
let toAddress = ''
|
||||
let tokenAmount = ''
|
||||
|
||||
if (params && params.length === 2) {
|
||||
[{ value: toAddress }, { value: tokenAmount }] = params
|
||||
}
|
||||
|
||||
const numberOfTokens = tokenAmount && tokenDecimals
|
||||
? calcTokenAmount(tokenAmount, tokenDecimals)
|
||||
: 0
|
||||
|
||||
return {
|
||||
toAddress,
|
||||
tokenAddress,
|
||||
tokenSymbol,
|
||||
numberOfTokens,
|
||||
}
|
||||
}
|
||||
|
||||
const mapDispatchToProps = dispatch => {
|
||||
return {
|
||||
editTransaction: ({ txData, tokenData, tokenProps }) => {
|
||||
const { txParams: { to: tokenAddress, gas: gasLimit, gasPrice } = {}, id } = txData
|
||||
const { params = [] } = tokenData
|
||||
const { value: to } = params[0] || {}
|
||||
const { value: tokenAmountInDec } = params[1] || {}
|
||||
const tokenAmountInHex = conversionUtil(tokenAmountInDec, {
|
||||
fromNumericBase: 'dec',
|
||||
toNumericBase: 'hex',
|
||||
})
|
||||
dispatch(setSelectedToken(tokenAddress))
|
||||
dispatch(updateSend({
|
||||
gasLimit,
|
||||
gasPrice,
|
||||
gasTotal: null,
|
||||
to,
|
||||
amount: tokenAmountInHex,
|
||||
errors: { to: null, amount: null },
|
||||
editingTransactionId: id && id.toString(),
|
||||
token: {
|
||||
...tokenProps,
|
||||
address: tokenAddress,
|
||||
},
|
||||
}))
|
||||
dispatch(clearConfirmTransaction())
|
||||
dispatch(showSendTokenPage())
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
export default compose(
|
||||
withRouter,
|
||||
connect(mapStateToProps, mapDispatchToProps)
|
||||
)(ConfirmSendToken)
|
1
ui/app/components/pages/confirm-send-token/index.js
Normal file
1
ui/app/components/pages/confirm-send-token/index.js
Normal file
@ -0,0 +1 @@
|
||||
export { default } from './confirm-send-token.container'
|
19
ui/app/components/pages/confirm-send-token/index.scss
Normal file
19
ui/app/components/pages/confirm-send-token/index.scss
Normal file
@ -0,0 +1,19 @@
|
||||
.confirm-send-token {
|
||||
&__title {
|
||||
padding: 4px 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
&__identicon {
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
&__title-text {
|
||||
font-size: 2.25rem;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
padding-left: 8px;
|
||||
}
|
||||
}
|
@ -0,0 +1,320 @@
|
||||
import React, { Component } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import ConfirmPageContainer, { ConfirmDetailRow } from '../../confirm-page-container'
|
||||
import { formatCurrency } from '../../../helpers/confirm-transaction/util'
|
||||
import { isBalanceSufficient } from '../../send_/send.utils'
|
||||
import { DEFAULT_ROUTE } from '../../../routes'
|
||||
import {
|
||||
INSUFFICIENT_FUNDS_ERROR_KEY,
|
||||
TRANSACTION_ERROR_KEY,
|
||||
} from '../../../constants/error-keys'
|
||||
|
||||
export default class ConfirmTransactionBase extends Component {
|
||||
static contextTypes = {
|
||||
t: PropTypes.func,
|
||||
}
|
||||
|
||||
static propTypes = {
|
||||
// react-router props
|
||||
match: PropTypes.object,
|
||||
history: PropTypes.object,
|
||||
// Redux props
|
||||
balance: PropTypes.string,
|
||||
cancelTransaction: PropTypes.func,
|
||||
clearConfirmTransaction: PropTypes.func,
|
||||
clearSend: PropTypes.func,
|
||||
conversionRate: PropTypes.number,
|
||||
currentCurrency: PropTypes.string,
|
||||
editTransaction: PropTypes.func,
|
||||
ethTransactionAmount: PropTypes.string,
|
||||
ethTransactionFee: PropTypes.string,
|
||||
ethTransactionTotal: PropTypes.string,
|
||||
fiatTransactionAmount: PropTypes.string,
|
||||
fiatTransactionFee: PropTypes.string,
|
||||
fiatTransactionTotal: PropTypes.string,
|
||||
fromAddress: PropTypes.string,
|
||||
fromName: PropTypes.string,
|
||||
hexGasTotal: PropTypes.string,
|
||||
isTxReprice: PropTypes.bool,
|
||||
methodData: PropTypes.object,
|
||||
nonce: PropTypes.string,
|
||||
sendTransaction: PropTypes.func,
|
||||
showCustomizeGasModal: PropTypes.func,
|
||||
showTransactionConfirmedModal: PropTypes.func,
|
||||
toAddress: PropTypes.string,
|
||||
tokenData: PropTypes.object,
|
||||
tokenProps: PropTypes.object,
|
||||
toName: PropTypes.string,
|
||||
transactionStatus: PropTypes.string,
|
||||
txData: PropTypes.object,
|
||||
// Component props
|
||||
action: PropTypes.string,
|
||||
contentComponent: PropTypes.node,
|
||||
dataComponent: PropTypes.node,
|
||||
detailsComponent: PropTypes.node,
|
||||
errorKey: PropTypes.string,
|
||||
errorMessage: PropTypes.string,
|
||||
hideData: PropTypes.bool,
|
||||
hideDetails: PropTypes.bool,
|
||||
hideSubtitle: PropTypes.bool,
|
||||
identiconAddress: PropTypes.string,
|
||||
onCancel: PropTypes.func,
|
||||
onEdit: PropTypes.func,
|
||||
onEditGas: PropTypes.func,
|
||||
onSubmit: PropTypes.func,
|
||||
subtitle: PropTypes.string,
|
||||
summaryComponent: PropTypes.node,
|
||||
title: PropTypes.string,
|
||||
valid: PropTypes.bool,
|
||||
warning: PropTypes.string,
|
||||
}
|
||||
|
||||
componentDidUpdate () {
|
||||
const {
|
||||
transactionStatus,
|
||||
showTransactionConfirmedModal,
|
||||
history,
|
||||
clearConfirmTransaction,
|
||||
} = this.props
|
||||
|
||||
if (transactionStatus === 'dropped') {
|
||||
showTransactionConfirmedModal({
|
||||
onHide: () => {
|
||||
clearConfirmTransaction()
|
||||
history.push(DEFAULT_ROUTE)
|
||||
},
|
||||
})
|
||||
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
getErrorKey () {
|
||||
const {
|
||||
balance,
|
||||
conversionRate,
|
||||
hexGasTotal,
|
||||
txData: {
|
||||
simulationFails,
|
||||
txParams: {
|
||||
value: amount,
|
||||
} = {},
|
||||
} = {},
|
||||
} = this.props
|
||||
|
||||
const insufficientBalance = balance && !isBalanceSufficient({
|
||||
amount,
|
||||
gasTotal: hexGasTotal || '0x0',
|
||||
balance,
|
||||
conversionRate,
|
||||
})
|
||||
|
||||
if (insufficientBalance) {
|
||||
return {
|
||||
valid: false,
|
||||
errorKey: INSUFFICIENT_FUNDS_ERROR_KEY,
|
||||
}
|
||||
}
|
||||
|
||||
if (simulationFails) {
|
||||
return {
|
||||
valid: false,
|
||||
errorKey: TRANSACTION_ERROR_KEY,
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
valid: true,
|
||||
}
|
||||
}
|
||||
|
||||
handleEditGas () {
|
||||
const { onEditGas, showCustomizeGasModal } = this.props
|
||||
|
||||
if (onEditGas) {
|
||||
onEditGas()
|
||||
} else {
|
||||
showCustomizeGasModal()
|
||||
}
|
||||
}
|
||||
|
||||
renderDetails () {
|
||||
const {
|
||||
detailsComponent,
|
||||
fiatTransactionFee,
|
||||
ethTransactionFee,
|
||||
currentCurrency,
|
||||
fiatTransactionTotal,
|
||||
ethTransactionTotal,
|
||||
hideDetails,
|
||||
} = this.props
|
||||
|
||||
if (hideDetails) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
detailsComponent || (
|
||||
<div className="confirm-page-container-content__details">
|
||||
<div className="confirm-page-container-content__gas-fee">
|
||||
<ConfirmDetailRow
|
||||
label="Gas Fee"
|
||||
fiatFee={formatCurrency(fiatTransactionFee, currentCurrency)}
|
||||
ethFee={ethTransactionFee}
|
||||
headerText="Edit"
|
||||
headerTextClassName="confirm-detail-row__header-text--edit"
|
||||
onHeaderClick={() => this.handleEditGas()}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<ConfirmDetailRow
|
||||
label="Total"
|
||||
fiatFee={formatCurrency(fiatTransactionTotal, currentCurrency)}
|
||||
ethFee={ethTransactionTotal}
|
||||
headerText="Amount + Gas Fee"
|
||||
headerTextClassName="confirm-detail-row__header-text--total"
|
||||
fiatFeeColor="#2f9ae0"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
renderData () {
|
||||
const { t } = this.context
|
||||
const {
|
||||
txData: {
|
||||
txParams: {
|
||||
data,
|
||||
} = {},
|
||||
} = {},
|
||||
methodData: {
|
||||
name,
|
||||
params,
|
||||
} = {},
|
||||
hideData,
|
||||
dataComponent,
|
||||
} = this.props
|
||||
|
||||
if (hideData) {
|
||||
return null
|
||||
}
|
||||
|
||||
return dataComponent || (
|
||||
<div className="confirm-page-container-content__data">
|
||||
<div className="confirm-page-container-content__data-box-label">
|
||||
{`${t('functionType')}:`}
|
||||
<span className="confirm-page-container-content__function-type">
|
||||
{ name }
|
||||
</span>
|
||||
</div>
|
||||
<div className="confirm-page-container-content__data-box">
|
||||
<div className="confirm-page-container-content__data-field-label">
|
||||
{ `${t('parameters')}:` }
|
||||
</div>
|
||||
<div>
|
||||
<pre>{ JSON.stringify(params, null, 2) }</pre>
|
||||
</div>
|
||||
</div>
|
||||
<div className="confirm-page-container-content__data-box-label">
|
||||
{`${t('hexData')}:`}
|
||||
</div>
|
||||
<div className="confirm-page-container-content__data-box">
|
||||
{ data }
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
handleEdit () {
|
||||
const { txData, tokenData, tokenProps, onEdit } = this.props
|
||||
onEdit({ txData, tokenData, tokenProps })
|
||||
}
|
||||
|
||||
handleCancel () {
|
||||
const { onCancel, txData, cancelTransaction, history, clearConfirmTransaction } = this.props
|
||||
|
||||
if (onCancel) {
|
||||
onCancel(txData)
|
||||
} else {
|
||||
cancelTransaction(txData)
|
||||
.then(() => {
|
||||
clearConfirmTransaction()
|
||||
history.push(DEFAULT_ROUTE)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
handleSubmit () {
|
||||
const { sendTransaction, clearConfirmTransaction, txData, history, onSubmit } = this.props
|
||||
|
||||
if (onSubmit) {
|
||||
onSubmit(txData)
|
||||
} else {
|
||||
sendTransaction(txData)
|
||||
.then(() => {
|
||||
clearConfirmTransaction()
|
||||
history.push(DEFAULT_ROUTE)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
render () {
|
||||
const {
|
||||
isTxReprice,
|
||||
fromName,
|
||||
fromAddress,
|
||||
toName,
|
||||
toAddress,
|
||||
methodData,
|
||||
ethTransactionAmount,
|
||||
fiatTransactionAmount,
|
||||
valid: propsValid,
|
||||
errorMessage,
|
||||
errorKey: propsErrorKey,
|
||||
currentCurrency,
|
||||
action,
|
||||
title,
|
||||
subtitle,
|
||||
hideSubtitle,
|
||||
identiconAddress,
|
||||
summaryComponent,
|
||||
contentComponent,
|
||||
onEdit,
|
||||
nonce,
|
||||
warning,
|
||||
} = this.props
|
||||
|
||||
const { name } = methodData
|
||||
const fiatConvertedAmount = formatCurrency(fiatTransactionAmount, currentCurrency)
|
||||
const { valid, errorKey } = this.getErrorKey()
|
||||
|
||||
return (
|
||||
<ConfirmPageContainer
|
||||
fromName={fromName}
|
||||
fromAddress={fromAddress}
|
||||
toName={toName}
|
||||
toAddress={toAddress}
|
||||
showEdit={onEdit && !isTxReprice}
|
||||
action={action || name}
|
||||
title={title || `${fiatConvertedAmount} ${currentCurrency.toUpperCase()}`}
|
||||
subtitle={subtitle || `\u2666 ${ethTransactionAmount}`}
|
||||
hideSubtitle={hideSubtitle}
|
||||
summaryComponent={summaryComponent}
|
||||
detailsComponent={this.renderDetails()}
|
||||
dataComponent={this.renderData()}
|
||||
contentComponent={contentComponent}
|
||||
nonce={nonce}
|
||||
identiconAddress={identiconAddress}
|
||||
errorMessage={errorMessage}
|
||||
errorKey={propsErrorKey || errorKey}
|
||||
warning={warning}
|
||||
valid={propsValid || valid}
|
||||
onEdit={() => this.handleEdit()}
|
||||
onCancel={() => this.handleCancel()}
|
||||
onSubmit={() => this.handleSubmit()}
|
||||
/>
|
||||
)
|
||||
}
|
||||
}
|
@ -0,0 +1,169 @@
|
||||
import { connect } from 'react-redux'
|
||||
import { compose } from 'recompose'
|
||||
import { withRouter } from 'react-router-dom'
|
||||
import R from 'ramda'
|
||||
import ConfirmTransactionBase from './confirm-transaction-base.component'
|
||||
import {
|
||||
clearConfirmTransaction,
|
||||
updateGasAndCalculate,
|
||||
} from '../../../ducks/confirm-transaction.duck'
|
||||
import { clearSend, cancelTx, updateAndApproveTx, showModal } from '../../../actions'
|
||||
import {
|
||||
INSUFFICIENT_FUNDS_ERROR_KEY,
|
||||
GAS_LIMIT_TOO_LOW_ERROR_KEY,
|
||||
} from '../../../constants/error-keys'
|
||||
import { getHexGasTotal } from '../../../helpers/confirm-transaction/util'
|
||||
import { isBalanceSufficient } from '../../send_/send.utils'
|
||||
import { conversionGreaterThan } from '../../../conversion-util'
|
||||
import { MIN_GAS_LIMIT_DEC } from '../../send_/send.constants'
|
||||
|
||||
const mapStateToProps = (state, props) => {
|
||||
const { toAddress: propsToAddress } = props
|
||||
const { confirmTransaction, metamask } = state
|
||||
const {
|
||||
ethTransactionAmount,
|
||||
ethTransactionFee,
|
||||
ethTransactionTotal,
|
||||
fiatTransactionAmount,
|
||||
fiatTransactionFee,
|
||||
fiatTransactionTotal,
|
||||
hexGasTotal,
|
||||
tokenData,
|
||||
methodData,
|
||||
txData,
|
||||
tokenProps,
|
||||
nonce,
|
||||
} = confirmTransaction
|
||||
const { txParams = {}, lastGasPrice, id: transactionId } = txData
|
||||
const { from: fromAddress, to: txParamsToAddress } = txParams
|
||||
const {
|
||||
conversionRate,
|
||||
identities,
|
||||
currentCurrency,
|
||||
accounts,
|
||||
selectedAddress,
|
||||
selectedAddressTxList,
|
||||
} = metamask
|
||||
|
||||
const { balance } = accounts[selectedAddress]
|
||||
const { name: fromName } = identities[selectedAddress]
|
||||
const toAddress = propsToAddress || txParamsToAddress
|
||||
const toName = identities[toAddress] && identities[toAddress].name
|
||||
const isTxReprice = Boolean(lastGasPrice)
|
||||
|
||||
const transaction = R.find(({ id }) => id === transactionId)(selectedAddressTxList)
|
||||
const transactionStatus = transaction ? transaction.status : ''
|
||||
|
||||
return {
|
||||
balance,
|
||||
fromAddress,
|
||||
fromName,
|
||||
toAddress,
|
||||
toName,
|
||||
ethTransactionAmount,
|
||||
ethTransactionFee,
|
||||
ethTransactionTotal,
|
||||
fiatTransactionAmount,
|
||||
fiatTransactionFee,
|
||||
fiatTransactionTotal,
|
||||
hexGasTotal,
|
||||
txData,
|
||||
tokenData,
|
||||
methodData,
|
||||
tokenProps,
|
||||
isTxReprice,
|
||||
currentCurrency,
|
||||
conversionRate,
|
||||
transactionStatus,
|
||||
nonce,
|
||||
}
|
||||
}
|
||||
|
||||
const mapDispatchToProps = dispatch => {
|
||||
return {
|
||||
clearConfirmTransaction: () => dispatch(clearConfirmTransaction()),
|
||||
clearSend: () => dispatch(clearSend()),
|
||||
showTransactionConfirmedModal: ({ onHide }) => {
|
||||
return dispatch(showModal({ name: 'TRANSACTION_CONFIRMED', onHide }))
|
||||
},
|
||||
showCustomizeGasModal: ({ txData, onSubmit, validate }) => {
|
||||
return dispatch(showModal({ name: 'CONFIRM_CUSTOMIZE_GAS', txData, onSubmit, validate }))
|
||||
},
|
||||
updateGasAndCalculate: ({ gasLimit, gasPrice }) => {
|
||||
return dispatch(updateGasAndCalculate({ gasLimit, gasPrice }))
|
||||
},
|
||||
cancelTransaction: ({ id }) => dispatch(cancelTx({ id })),
|
||||
sendTransaction: txData => dispatch(updateAndApproveTx(txData)),
|
||||
}
|
||||
}
|
||||
|
||||
const getValidateEditGas = ({ balance, conversionRate, txData }) => {
|
||||
const { txParams: { value: amount } = {} } = txData
|
||||
|
||||
return ({ gasLimit, gasPrice }) => {
|
||||
const gasTotal = getHexGasTotal({ gasLimit, gasPrice })
|
||||
const hasSufficientBalance = isBalanceSufficient({
|
||||
amount,
|
||||
gasTotal,
|
||||
balance,
|
||||
conversionRate,
|
||||
})
|
||||
|
||||
if (!hasSufficientBalance) {
|
||||
return {
|
||||
valid: false,
|
||||
errorKey: INSUFFICIENT_FUNDS_ERROR_KEY,
|
||||
}
|
||||
}
|
||||
|
||||
const gasLimitTooLow = gasLimit && conversionGreaterThan(
|
||||
{
|
||||
value: MIN_GAS_LIMIT_DEC,
|
||||
fromNumericBase: 'dec',
|
||||
conversionRate,
|
||||
},
|
||||
{
|
||||
value: gasLimit,
|
||||
fromNumericBase: 'hex',
|
||||
},
|
||||
)
|
||||
|
||||
if (gasLimitTooLow) {
|
||||
return {
|
||||
valid: false,
|
||||
errorKey: GAS_LIMIT_TOO_LOW_ERROR_KEY,
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
valid: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const mergeProps = (stateProps, dispatchProps, ownProps) => {
|
||||
const { balance, conversionRate, txData } = stateProps
|
||||
const {
|
||||
showCustomizeGasModal: dispatchShowCustomizeGasModal,
|
||||
updateGasAndCalculate: dispatchUpdateGasAndCalculate,
|
||||
...otherDispatchProps
|
||||
} = dispatchProps
|
||||
|
||||
const validateEditGas = getValidateEditGas({ balance, conversionRate, txData })
|
||||
|
||||
return {
|
||||
...stateProps,
|
||||
...otherDispatchProps,
|
||||
...ownProps,
|
||||
showCustomizeGasModal: () => dispatchShowCustomizeGasModal({
|
||||
txData,
|
||||
onSubmit: txData => dispatchUpdateGasAndCalculate(txData),
|
||||
validate: validateEditGas,
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
export default compose(
|
||||
withRouter,
|
||||
connect(mapStateToProps, mapDispatchToProps, mergeProps)
|
||||
)(ConfirmTransactionBase)
|
@ -0,0 +1 @@
|
||||
export { default } from './confirm-transaction-base.container'
|
@ -0,0 +1,77 @@
|
||||
import React, { Component } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import { Redirect } from 'react-router-dom'
|
||||
import Loading from '../../loading-screen'
|
||||
import {
|
||||
CONFIRM_TRANSACTION_ROUTE,
|
||||
CONFIRM_DEPLOY_CONTRACT_PATH,
|
||||
CONFIRM_SEND_ETHER_PATH,
|
||||
CONFIRM_SEND_TOKEN_PATH,
|
||||
CONFIRM_APPROVE_PATH,
|
||||
CONFIRM_TOKEN_METHOD_PATH,
|
||||
SIGNATURE_REQUEST_PATH,
|
||||
} from '../../../routes'
|
||||
import { isConfirmDeployContract } from './confirm-transaction-switch.util'
|
||||
import { TOKEN_METHOD_TRANSFER, TOKEN_METHOD_APPROVE } from './confirm-transaction-switch.constants'
|
||||
|
||||
export default class ConfirmTransactionSwitch extends Component {
|
||||
static propTypes = {
|
||||
txData: PropTypes.object,
|
||||
methodData: PropTypes.object,
|
||||
fetchingMethodData: PropTypes.bool,
|
||||
}
|
||||
|
||||
redirectToTransaction () {
|
||||
const {
|
||||
txData,
|
||||
methodData: { name },
|
||||
fetchingMethodData,
|
||||
} = this.props
|
||||
const { id } = txData
|
||||
|
||||
|
||||
if (isConfirmDeployContract(txData)) {
|
||||
const pathname = `${CONFIRM_TRANSACTION_ROUTE}/${id}${CONFIRM_DEPLOY_CONTRACT_PATH}`
|
||||
return <Redirect to={{ pathname }} />
|
||||
}
|
||||
|
||||
if (fetchingMethodData) {
|
||||
return <Loading />
|
||||
}
|
||||
|
||||
if (name) {
|
||||
const methodName = name.toLowerCase()
|
||||
|
||||
switch (methodName.toLowerCase()) {
|
||||
case TOKEN_METHOD_TRANSFER: {
|
||||
const pathname = `${CONFIRM_TRANSACTION_ROUTE}/${id}${CONFIRM_SEND_TOKEN_PATH}`
|
||||
return <Redirect to={{ pathname }} />
|
||||
}
|
||||
case TOKEN_METHOD_APPROVE: {
|
||||
const pathname = `${CONFIRM_TRANSACTION_ROUTE}/${id}${CONFIRM_APPROVE_PATH}`
|
||||
return <Redirect to={{ pathname }} />
|
||||
}
|
||||
default: {
|
||||
const pathname = `${CONFIRM_TRANSACTION_ROUTE}/${id}${CONFIRM_TOKEN_METHOD_PATH}`
|
||||
return <Redirect to={{ pathname }} />
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const pathname = `${CONFIRM_TRANSACTION_ROUTE}/${id}${CONFIRM_SEND_ETHER_PATH}`
|
||||
return <Redirect to={{ pathname }} />
|
||||
}
|
||||
|
||||
render () {
|
||||
const { txData } = this.props
|
||||
|
||||
if (txData.txParams) {
|
||||
return this.redirectToTransaction()
|
||||
} else if (txData.msgParams) {
|
||||
const pathname = `${CONFIRM_TRANSACTION_ROUTE}/${txData.id}${SIGNATURE_REQUEST_PATH}`
|
||||
return <Redirect to={{ pathname }} />
|
||||
}
|
||||
|
||||
return <Loading />
|
||||
}
|
||||
}
|
@ -0,0 +1,2 @@
|
||||
export const TOKEN_METHOD_TRANSFER = 'transfer'
|
||||
export const TOKEN_METHOD_APPROVE = 'approve'
|
@ -0,0 +1,20 @@
|
||||
import { connect } from 'react-redux'
|
||||
import ConfirmTransactionSwitch from './confirm-transaction-switch.component'
|
||||
|
||||
const mapStateToProps = state => {
|
||||
const {
|
||||
confirmTransaction: {
|
||||
txData,
|
||||
methodData,
|
||||
fetchingMethodData,
|
||||
},
|
||||
} = state
|
||||
|
||||
return {
|
||||
txData,
|
||||
methodData,
|
||||
fetchingMethodData,
|
||||
}
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps)(ConfirmTransactionSwitch)
|
@ -0,0 +1,4 @@
|
||||
export function isConfirmDeployContract (txData = {}) {
|
||||
const { txParams = {} } = txData
|
||||
return !txParams.to
|
||||
}
|
@ -0,0 +1,2 @@
|
||||
import ConfirmTransactionSwitch from './confirm-transaction-switch.container'
|
||||
module.exports = ConfirmTransactionSwitch
|
@ -0,0 +1,150 @@
|
||||
import React, { Component } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import { Switch, Route } from 'react-router-dom'
|
||||
import Loading from '../../loading-screen'
|
||||
import ConfirmTransactionSwitch from '../confirm-transaction-switch'
|
||||
import ConfirmTransactionBase from '../confirm-transaction-base'
|
||||
import ConfirmSendEther from '../confirm-send-ether'
|
||||
import ConfirmSendToken from '../confirm-send-token'
|
||||
import ConfirmDeployContract from '../confirm-deploy-contract'
|
||||
import ConfirmApprove from '../confirm-approve'
|
||||
import ConfTx from '../../../conf-tx'
|
||||
import {
|
||||
DEFAULT_ROUTE,
|
||||
CONFIRM_TRANSACTION_ROUTE,
|
||||
CONFIRM_DEPLOY_CONTRACT_PATH,
|
||||
CONFIRM_SEND_ETHER_PATH,
|
||||
CONFIRM_SEND_TOKEN_PATH,
|
||||
CONFIRM_APPROVE_PATH,
|
||||
CONFIRM_TOKEN_METHOD_PATH,
|
||||
SIGNATURE_REQUEST_PATH,
|
||||
} from '../../../routes'
|
||||
|
||||
export default class ConfirmTransaction extends Component {
|
||||
static propTypes = {
|
||||
history: PropTypes.object.isRequired,
|
||||
totalUnapprovedCount: PropTypes.number.isRequired,
|
||||
match: PropTypes.object,
|
||||
send: PropTypes.object,
|
||||
unconfirmedTransactions: PropTypes.array,
|
||||
setTransactionToConfirm: PropTypes.func,
|
||||
confirmTransaction: PropTypes.object,
|
||||
clearConfirmTransaction: PropTypes.func,
|
||||
}
|
||||
|
||||
getParamsTransactionId () {
|
||||
const { match: { params: { id } = {} } } = this.props
|
||||
return id || null
|
||||
}
|
||||
|
||||
componentDidMount () {
|
||||
const {
|
||||
totalUnapprovedCount = 0,
|
||||
send = {},
|
||||
history,
|
||||
confirmTransaction: { txData: { id: transactionId } = {} },
|
||||
} = this.props
|
||||
|
||||
if (!totalUnapprovedCount && !send.to) {
|
||||
history.replace(DEFAULT_ROUTE)
|
||||
return
|
||||
}
|
||||
|
||||
if (!transactionId) {
|
||||
this.setTransactionToConfirm()
|
||||
}
|
||||
}
|
||||
|
||||
componentDidUpdate () {
|
||||
const {
|
||||
setTransactionToConfirm,
|
||||
confirmTransaction: { txData: { id: transactionId } = {} },
|
||||
clearConfirmTransaction,
|
||||
} = this.props
|
||||
const paramsTransactionId = this.getParamsTransactionId()
|
||||
|
||||
if (paramsTransactionId && transactionId && paramsTransactionId !== transactionId + '') {
|
||||
clearConfirmTransaction()
|
||||
setTransactionToConfirm(paramsTransactionId)
|
||||
return
|
||||
}
|
||||
|
||||
if (!transactionId) {
|
||||
this.setTransactionToConfirm()
|
||||
}
|
||||
}
|
||||
|
||||
setTransactionToConfirm () {
|
||||
const {
|
||||
history,
|
||||
unconfirmedTransactions,
|
||||
setTransactionToConfirm,
|
||||
} = this.props
|
||||
const paramsTransactionId = this.getParamsTransactionId()
|
||||
|
||||
if (paramsTransactionId) {
|
||||
// Check to make sure params ID is valid
|
||||
const tx = unconfirmedTransactions.find(({ id }) => id + '' === paramsTransactionId)
|
||||
|
||||
if (!tx) {
|
||||
history.replace(DEFAULT_ROUTE)
|
||||
} else {
|
||||
setTransactionToConfirm(paramsTransactionId)
|
||||
}
|
||||
} else if (unconfirmedTransactions.length) {
|
||||
const totalUnconfirmed = unconfirmedTransactions.length
|
||||
const transaction = unconfirmedTransactions[totalUnconfirmed - 1]
|
||||
const { id: transactionId, loadingDefaults } = transaction
|
||||
|
||||
if (!loadingDefaults) {
|
||||
setTransactionToConfirm(transactionId)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
render () {
|
||||
const { confirmTransaction: { txData: { id } } = {} } = this.props
|
||||
const paramsTransactionId = this.getParamsTransactionId()
|
||||
|
||||
// Show routes when state.confirmTransaction has been set and when either the ID in the params
|
||||
// isn't specified or is specified and matches the ID in state.confirmTransaction in order to
|
||||
// support URLs of /confirm-transaction or /confirm-transaction/<transactionId>
|
||||
return id && (!paramsTransactionId || paramsTransactionId === id + '')
|
||||
? (
|
||||
<Switch>
|
||||
<Route
|
||||
exact
|
||||
path={`${CONFIRM_TRANSACTION_ROUTE}/:id?${CONFIRM_DEPLOY_CONTRACT_PATH}`}
|
||||
component={ConfirmDeployContract}
|
||||
/>
|
||||
<Route
|
||||
exact
|
||||
path={`${CONFIRM_TRANSACTION_ROUTE}/:id?${CONFIRM_TOKEN_METHOD_PATH}`}
|
||||
component={ConfirmTransactionBase}
|
||||
/>
|
||||
<Route
|
||||
exact
|
||||
path={`${CONFIRM_TRANSACTION_ROUTE}/:id?${CONFIRM_SEND_ETHER_PATH}`}
|
||||
component={ConfirmSendEther}
|
||||
/>
|
||||
<Route
|
||||
exact
|
||||
path={`${CONFIRM_TRANSACTION_ROUTE}/:id?${CONFIRM_SEND_TOKEN_PATH}`}
|
||||
component={ConfirmSendToken}
|
||||
/>
|
||||
<Route
|
||||
exact
|
||||
path={`${CONFIRM_TRANSACTION_ROUTE}/:id?${CONFIRM_APPROVE_PATH}`}
|
||||
component={ConfirmApprove}
|
||||
/>
|
||||
<Route
|
||||
exact
|
||||
path={`${CONFIRM_TRANSACTION_ROUTE}/:id?${SIGNATURE_REQUEST_PATH}`}
|
||||
component={ConfTx}
|
||||
/>
|
||||
<Route path="*" component={ConfirmTransactionSwitch} />
|
||||
</Switch>
|
||||
)
|
||||
: <Loading />
|
||||
}
|
||||
}
|
@ -0,0 +1,33 @@
|
||||
import { connect } from 'react-redux'
|
||||
import { compose } from 'recompose'
|
||||
import { withRouter } from 'react-router-dom'
|
||||
import {
|
||||
setTransactionToConfirm,
|
||||
clearConfirmTransaction,
|
||||
} from '../../../ducks/confirm-transaction.duck'
|
||||
import ConfirmTransaction from './confirm-transaction.component'
|
||||
import { getTotalUnapprovedCount } from '../../../selectors'
|
||||
import { unconfirmedTransactionsListSelector } from '../../../selectors/confirm-transaction'
|
||||
|
||||
const mapStateToProps = state => {
|
||||
const { metamask: { send }, confirmTransaction } = state
|
||||
|
||||
return {
|
||||
totalUnapprovedCount: getTotalUnapprovedCount(state),
|
||||
send,
|
||||
confirmTransaction,
|
||||
unconfirmedTransactions: unconfirmedTransactionsListSelector(state),
|
||||
}
|
||||
}
|
||||
|
||||
const mapDispatchToProps = dispatch => {
|
||||
return {
|
||||
setTransactionToConfirm: transactionId => dispatch(setTransactionToConfirm(transactionId)),
|
||||
clearConfirmTransaction: () => dispatch(clearConfirmTransaction()),
|
||||
}
|
||||
}
|
||||
|
||||
export default compose(
|
||||
withRouter,
|
||||
connect(mapStateToProps, mapDispatchToProps),
|
||||
)(ConfirmTransaction)
|
2
ui/app/components/pages/confirm-transaction/index.js
Normal file
2
ui/app/components/pages/confirm-transaction/index.js
Normal file
@ -0,0 +1,2 @@
|
||||
import ConfirmTransaction from './confirm-transaction.container'
|
||||
module.exports = ConfirmTransaction
|
@ -83,51 +83,6 @@ class Home extends Component {
|
||||
})
|
||||
}
|
||||
|
||||
// if (!props.noActiveNotices) {
|
||||
// log.debug('rendering notice screen for unread notices.')
|
||||
// return h(NoticeScreen, {
|
||||
// notice: props.nextUnreadNotice,
|
||||
// key: 'NoticeScreen',
|
||||
// onConfirm: () => props.dispatch(actions.markNoticeRead(props.nextUnreadNotice)),
|
||||
// })
|
||||
// } else if (props.lostAccounts && props.lostAccounts.length > 0) {
|
||||
// log.debug('rendering notice screen for lost accounts view.')
|
||||
// return h(NoticeScreen, {
|
||||
// notice: generateLostAccountsNotice(props.lostAccounts),
|
||||
// key: 'LostAccountsNotice',
|
||||
// onConfirm: () => props.dispatch(actions.markAccountsFound()),
|
||||
// })
|
||||
// }
|
||||
|
||||
// if (props.seedWords) {
|
||||
// log.debug('rendering seed words')
|
||||
// return h(HDCreateVaultComplete, {key: 'HDCreateVaultComplete'})
|
||||
// }
|
||||
|
||||
// show initialize screen
|
||||
// if (!isInitialized || forgottenPassword) {
|
||||
// // show current view
|
||||
// log.debug('rendering an initialize screen')
|
||||
// // switch (props.currentView.name) {
|
||||
|
||||
// // case 'restoreVault':
|
||||
// // log.debug('rendering restore vault screen')
|
||||
// // return h(HDRestoreVaultScreen, {key: 'HDRestoreVaultScreen'})
|
||||
|
||||
// // default:
|
||||
// // log.debug('rendering menu screen')
|
||||
// // return h(InitializeScreen, {key: 'menuScreenInit'})
|
||||
// // }
|
||||
// }
|
||||
|
||||
// // show unlock screen
|
||||
// if (!props.isUnlocked) {
|
||||
// return h(MainContainer, {
|
||||
// currentViewName: props.currentView.name,
|
||||
// isUnlocked: props.isUnlocked,
|
||||
// })
|
||||
// }
|
||||
|
||||
// show current view
|
||||
switch (currentView.name) {
|
||||
|
||||
@ -135,59 +90,10 @@ class Home extends Component {
|
||||
log.debug('rendering main container')
|
||||
return h(MainContainer, {key: 'account-detail'})
|
||||
|
||||
// case 'sendTransaction':
|
||||
// log.debug('rendering send tx screen')
|
||||
|
||||
// // Going to leave this here until we are ready to delete SendTransactionScreen v1
|
||||
// // const SendComponentToRender = checkFeatureToggle('send-v2')
|
||||
// // ? SendTransactionScreen2
|
||||
// // : SendTransactionScreen
|
||||
|
||||
// return h(SendTransactionScreen2, {key: 'send-transaction'})
|
||||
|
||||
// case 'sendToken':
|
||||
// log.debug('rendering send token screen')
|
||||
|
||||
// // Going to leave this here until we are ready to delete SendTransactionScreen v1
|
||||
// // const SendTokenComponentToRender = checkFeatureToggle('send-v2')
|
||||
// // ? SendTransactionScreen2
|
||||
// // : SendTokenScreen
|
||||
|
||||
// return h(SendTransactionScreen2, {key: 'sendToken'})
|
||||
|
||||
case 'newKeychain':
|
||||
log.debug('rendering new keychain screen')
|
||||
return h(NewKeyChainScreen, {key: 'new-keychain'})
|
||||
|
||||
// case 'confTx':
|
||||
// log.debug('rendering confirm tx screen')
|
||||
// return h(Redirect, {
|
||||
// to: {
|
||||
// pathname: CONFIRM_TRANSACTION_ROUTE,
|
||||
// },
|
||||
// })
|
||||
// return h(ConfirmTxScreen, {key: 'confirm-tx'})
|
||||
|
||||
// case 'add-token':
|
||||
// log.debug('rendering add-token screen from unlock screen.')
|
||||
// return h(AddTokenScreen, {key: 'add-token'})
|
||||
|
||||
// case 'config':
|
||||
// log.debug('rendering config screen')
|
||||
// return h(Settings, {key: 'config'})
|
||||
|
||||
// case 'import-menu':
|
||||
// log.debug('rendering import screen')
|
||||
// return h(Import, {key: 'import-menu'})
|
||||
|
||||
// case 'reveal-seed-conf':
|
||||
// log.debug('rendering reveal seed confirmation screen')
|
||||
// return h(RevealSeedConfirmation, {key: 'reveal-seed-conf'})
|
||||
|
||||
// case 'info':
|
||||
// log.debug('rendering info screen')
|
||||
// return h(Settings, {key: 'info', tab: 'info'})
|
||||
|
||||
case 'buyEth':
|
||||
log.debug('rendering buy ether screen')
|
||||
return h(BuyView, {key: 'buyEthView'})
|
||||
|
@ -3,3 +3,5 @@
|
||||
@import './add-token/index';
|
||||
|
||||
@import './confirm-add-token/index';
|
||||
|
||||
@import './confirm-send-token/index';
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user