Merge pull request #7351 from MetaMask/Version-v7.5.0

Version v7.5.0 RC
feature/default_network_editable
Thomas Huang 5 years ago committed by GitHub
commit c594bb340d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 2
      .circleci/scripts/firefox-install
  2. 18
      CHANGELOG.md
  3. 53
      app/_locales/en/messages.json
  4. 3
      app/images/user-check.svg
  5. 2
      app/manifest.json
  6. 25
      app/scripts/controllers/ens/ens.js
  7. 94
      app/scripts/controllers/ens/index.js
  8. 15
      app/scripts/controllers/network/createMetamaskMiddleware.js
  9. 28
      app/scripts/controllers/network/middleware/pending.js
  10. 21
      app/scripts/controllers/network/util.js
  11. 1
      app/scripts/controllers/preferences.js
  12. 22
      app/scripts/controllers/transactions/index.js
  13. 4
      app/scripts/controllers/transactions/pending-tx-tracker.js
  14. 2
      app/scripts/lib/ens-ipfs/setup.js
  15. 12
      app/scripts/metamask-controller.js
  16. 7
      package.json
  17. 24
      test/e2e/address-book.spec.js
  18. 6
      test/e2e/from-import-ui.spec.js
  19. 2
      test/e2e/func.js
  20. 12
      test/e2e/metamask-responsive-ui.spec.js
  21. 168
      test/e2e/metamask-ui.spec.js
  22. 33
      test/e2e/run-all.sh
  23. 6
      test/e2e/send-edit.spec.js
  24. 135
      test/unit/app/controllers/ens-controller-test.js
  25. 4
      test/unit/app/controllers/network/network-controller-test.js
  26. 81
      test/unit/app/controllers/network/pending-middleware-test.js
  27. 225
      test/unit/app/controllers/network/stubs.js
  28. 10
      test/unit/app/controllers/transactions/tx-controller-test.js
  29. 6
      test/unit/ui/app/selectors.spec.js
  30. 3
      ui/app/components/app/account-menu/index.scss
  31. 1
      ui/app/components/app/confirm-page-container/confirm-page-container-content/index.scss
  32. 55
      ui/app/components/app/confirm-page-container/confirm-page-container-header/confirm-page-container-header.component.js
  33. 13
      ui/app/components/app/confirm-page-container/confirm-page-container-header/index.scss
  34. 28
      ui/app/components/app/confirm-page-container/confirm-page-container.component.js
  35. 6
      ui/app/components/app/confirm-page-container/index.scss
  36. 4
      ui/app/components/app/gas-customization/advanced-gas-inputs/advanced-gas-inputs.component.js
  37. 2
      ui/app/components/app/gas-customization/advanced-gas-inputs/advanced-gas-inputs.container.js
  38. 10
      ui/app/components/app/gas-customization/gas-modal-page-container/advanced-tab-content/advanced-tab-content.component.js
  39. 49
      ui/app/components/app/gas-customization/gas-modal-page-container/gas-modal-page-container.container.js
  40. 32
      ui/app/components/app/gas-customization/gas-price-chart/gas-price-chart.utils.js
  41. 2
      ui/app/components/app/index.scss
  42. 9
      ui/app/components/app/modal/modal.component.js
  43. 170
      ui/app/components/app/modals/edit-approval-permission/edit-approval-permission.component.js
  44. 18
      ui/app/components/app/modals/edit-approval-permission/edit-approval-permission.container.js
  45. 1
      ui/app/components/app/modals/edit-approval-permission/index.js
  46. 167
      ui/app/components/app/modals/edit-approval-permission/index.scss
  47. 2
      ui/app/components/app/modals/index.scss
  48. 26
      ui/app/components/app/modals/modal.js
  49. 13
      ui/app/components/app/multiple-notifications/multiple-notifications.component.js
  50. 28
      ui/app/components/app/provider-page-container/provider-page-container.component.js
  51. 60
      ui/app/components/app/signature-request-original.js
  52. 1
      ui/app/components/app/signature-request/index.js
  53. 96
      ui/app/components/app/signature-request/index.scss
  54. 1
      ui/app/components/app/signature-request/signature-request-footer/index.js
  55. 18
      ui/app/components/app/signature-request/signature-request-footer/index.scss
  56. 24
      ui/app/components/app/signature-request/signature-request-footer/signature-request-footer.component.js
  57. 1
      ui/app/components/app/signature-request/signature-request-header/index.js
  58. 25
      ui/app/components/app/signature-request/signature-request-header/index.scss
  59. 29
      ui/app/components/app/signature-request/signature-request-header/signature-request-header.component.js
  60. 1
      ui/app/components/app/signature-request/signature-request-message/index.js
  61. 67
      ui/app/components/app/signature-request/signature-request-message/index.scss
  62. 50
      ui/app/components/app/signature-request/signature-request-message/signature-request-message.component.js
  63. 81
      ui/app/components/app/signature-request/signature-request.component.js
  64. 3
      ui/app/components/app/signature-request/signature-request.constants.js
  65. 72
      ui/app/components/app/signature-request/signature-request.container.js
  66. 25
      ui/app/components/app/signature-request/tests/signature-request.test.js
  67. 2
      ui/app/components/app/transaction-list-item-details/index.js
  68. 24
      ui/app/components/app/transaction-list-item-details/transaction-list-item-details.component.js
  69. 28
      ui/app/components/app/transaction-list-item-details/transaction-list-item-details.container.js
  70. 24
      ui/app/components/app/transaction-list-item/index.scss
  71. 35
      ui/app/components/app/transaction-list-item/transaction-list-item.component.js
  72. 14
      ui/app/components/app/transaction-list-item/transaction-list-item.container.js
  73. 38
      ui/app/components/app/transaction-list/transaction-list.component.js
  74. 13
      ui/app/components/app/transaction-list/transaction-list.container.js
  75. 1
      ui/app/components/app/transaction-time-remaining/index.js
  76. 52
      ui/app/components/app/transaction-time-remaining/transaction-time-remaining.component.js
  77. 41
      ui/app/components/app/transaction-time-remaining/transaction-time-remaining.container.js
  78. 13
      ui/app/components/app/transaction-time-remaining/transaction-time-remaining.util.js
  79. 42
      ui/app/components/ui/sender-to-recipient/sender-to-recipient.component.js
  80. 7
      ui/app/components/ui/tooltip-v2.js
  81. 4
      ui/app/css/itcss/tools/utilities.scss
  82. 9
      ui/app/helpers/higher-order-components/with-token-tracker/with-token-tracker.component.js
  83. 99
      ui/app/helpers/utils/gas-time-estimates.util.js
  84. 5
      ui/app/helpers/utils/token-util.js
  85. 223
      ui/app/pages/confirm-approve/confirm-approve-content/confirm-approve-content.component.js
  86. 1
      ui/app/pages/confirm-approve/confirm-approve-content/index.js
  87. 306
      ui/app/pages/confirm-approve/confirm-approve-content/index.scss
  88. 99
      ui/app/pages/confirm-approve/confirm-approve.component.js
  89. 97
      ui/app/pages/confirm-approve/confirm-approve.container.js
  90. 28
      ui/app/pages/confirm-approve/confirm-approve.util.js
  91. 1
      ui/app/pages/confirm-approve/index.scss
  92. 58
      ui/app/pages/confirm-transaction-base/confirm-transaction-base.component.js
  93. 25
      ui/app/pages/confirm-transaction-base/confirm-transaction-base.container.js
  94. 50
      ui/app/pages/confirm-transaction/conf-tx.js
  95. 2
      ui/app/pages/confirm-transaction/confirm-transaction.component.js
  96. 2
      ui/app/pages/confirm-transaction/confirm-transaction.container.js
  97. 7
      ui/app/pages/first-time-flow/create-password/import-with-seed-phrase/import-with-seed-phrase.component.js
  98. 2
      ui/app/pages/first-time-flow/index.scss
  99. 41
      ui/app/pages/home/home.component.js
  100. 2
      ui/app/pages/index.scss
  101. Some files were not shown because too many files have changed in this diff Show More

@ -4,7 +4,7 @@ set -e
set -u
set -o pipefail
FIREFOX_VERSION='68.0'
FIREFOX_VERSION='70.0'
FIREFOX_BINARY="firefox-${FIREFOX_VERSION}.tar.bz2"
FIREFOX_BINARY_URL="https://ftp.mozilla.org/pub/firefox/releases/${FIREFOX_VERSION}/linux-x86_64/en-US/${FIREFOX_BINARY}"
FIREFOX_PATH='/opt/firefox'

@ -2,6 +2,24 @@
## Current Develop Branch
## 7.5.0 Mon Nov 04 2019
- [#7328](https://github.com/MetaMask/metamask-extension/pull/7328): ignore known transactions on first broadcast and continue with normal flow
- [#7327](https://github.com/MetaMask/metamask-extension/pull/7327): eth_getTransactionByHash will now check metamask's local history for pending transactions
- [#7333](https://github.com/MetaMask/metamask-extension/pull/7333): Cleanup beforeunload handler after transaction is resolved
- [#7038](https://github.com/MetaMask/metamask-extension/pull/7038): Add support for ZeroNet
- [#7334](https://github.com/MetaMask/metamask-extension/pull/7334): Add web3 deprecation warning
- [#6924](https://github.com/MetaMask/metamask-extension/pull/6924): Add Estimated time to pending tx
- [#7177](https://github.com/MetaMask/metamask-extension/pull/7177): ENS Reverse Resolution support
- [#6891](https://github.com/MetaMask/metamask-extension/pull/6891): New signature request v3 UI
- [#7348](https://github.com/MetaMask/metamask-extension/pull/7348): fix width in first time flow button
- [#7271](https://github.com/MetaMask/metamask-extension/pull/7271): Redesign approve screen
- [#7354](https://github.com/MetaMask/metamask-extension/pull/7354): fix account menu width
- [#7379](https://github.com/MetaMask/metamask-extension/pull/7379): Set default advanced tab gas limit
- [#7380](https://github.com/MetaMask/metamask-extension/pull/7380): Fix advanced tab gas chart
- [#7374](https://github.com/MetaMask/metamask-extension/pull/7374): Hide accounts dropdown scrollbars on Firefox
- [#7357](https://github.com/MetaMask/metamask-extension/pull/7357): Update to gaba@1.8.0
- [#7335](https://github.com/MetaMask/metamask-extension/pull/7335): Add onbeforeunload and have it call onCancel
## 7.4.0 Tue Oct 29 2019
- [#7186](https://github.com/MetaMask/metamask-extension/pull/7186): Use `AdvancedGasInputs` in `AdvancedTabContent`
- [#7304](https://github.com/MetaMask/metamask-extension/pull/7304): Move signTypedData signing out to keyrings

@ -56,6 +56,10 @@
"acceleratingATransaction": {
"message": "* Accelerating a transaction by using a higher gas price increases its chances of getting processed by the network faster, but it is not always guaranteed."
},
"accessAndSpendNotice": {
"message": "$1 may access and spend up to this max amount",
"description": "$1 is the url of the site requesting ability to spend"
},
"accessingYourCamera": {
"message": "Accessing your camera..."
},
@ -113,9 +117,20 @@
"addAcquiredTokens": {
"message": "Add the tokens you've acquired using MetaMask"
},
"allowOriginSpendToken": {
"message": "Allow $1 to spend your $2?",
"description": "$1 is the url of the site and $2 is the symbol of the token they are requesting to spend"
},
"allowWithdrawAndSpend": {
"message": "Allow $1 to withdraw and spend up to the following amount:",
"description": "The url of the site that requested permission to 'withdraw and spend'"
},
"amount": {
"message": "Amount"
},
"amountWithColon": {
"message": "Amount:"
},
"appDescription": {
"message": "An Ethereum Wallet in your Browser",
"description": "The description of the application"
@ -384,6 +399,9 @@
"customRPC": {
"message": "Custom RPC"
},
"customSpendLimit": {
"message": "Custom Spend Limit"
},
"dataBackupFoundInfo": {
"message": "Some of your account data was backed up during a previous installation of MetaMask. This could include your settings, contacts and tokens. Would you like to restore this data now?"
},
@ -444,6 +462,9 @@
"editContact": {
"message": "Edit Contact"
},
"editPermission": {
"message": "Edit Permission"
},
"emailUs": {
"message": "Email us!"
},
@ -486,6 +507,9 @@
"enterAnAlias": {
"message": "Enter an alias"
},
"enterMaxSpendLimit": {
"message": "Enter Max Spend Limit"
},
"enterPassword": {
"message": "Enter password"
},
@ -516,6 +540,9 @@
"faster": {
"message": "Faster"
},
"feeAssociatedRequest": {
"message": "A fee is associated with this request."
},
"fiat": {
"message": "Fiat",
"description": "Exchange type"
@ -533,6 +560,9 @@
"fromShapeShift": {
"message": "From ShapeShift"
},
"functionApprove": {
"message": "Function: Approve"
},
"functionType": {
"message": "Function Type"
},
@ -953,6 +983,9 @@
"privateNetwork": {
"message": "Private Network"
},
"proposedApprovalLimit": {
"message": "Proposed Approval Limit"
},
"qrCode": {
"message": "Show QR Code"
},
@ -1188,6 +1221,9 @@
"signatureRequest": {
"message": "Signature Request"
},
"signatureRequest1": {
"message": "Message"
},
"signed": {
"message": "Signed"
},
@ -1209,6 +1245,13 @@
"speedUpTransaction": {
"message": "Speed up this transaction"
},
"spendLimitPermission": {
"message": "Spend limit permission"
},
"spendLimitRequestedBy": {
"message": "Spend limit requested by $1",
"description": "Origin of the site requesting the spend limit"
},
"switchNetworks": {
"message": "Switch Networks"
},
@ -1305,6 +1348,9 @@
"to": {
"message": "To"
},
"toWithColon": {
"message": "To:"
},
"toETHviaShapeShift": {
"message": "$1 to ETH via ShapeShift",
"description": "system will fill in deposit type in start of message"
@ -1379,6 +1425,10 @@
"message": "We had trouble loading your token balances. You can view them ",
"description": "Followed by a link (here) to view token balances"
},
"trustSiteApprovePermission": {
"message": "Do you trust this site? By granting this permission, you’re allowing $1 to withdraw your $2 and automate transactions for you.",
"description": "$1 is the url requesting permission and $2 is the symbol of the currency that the request is for"
},
"tryAgain": {
"message": "Try again"
},
@ -1406,6 +1456,9 @@
"unknownCameraError": {
"message": "There was an error while trying to access your camera. Please try again..."
},
"unlimited": {
"message": "Unlimited"
},
"unlock": {
"message": "Unlock"
},

@ -0,0 +1,3 @@
<svg width="20" height="16" viewBox="0 0 20 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M7 8C9.1875 8 11 6.21875 11 4C11 1.8125 9.1875 0 7 0C4.78125 0 3 1.8125 3 4C3 6.21875 4.78125 8 7 8ZM9.78125 9H9.25C8.5625 9.34375 7.8125 9.5 7 9.5C6.1875 9.5 5.40625 9.34375 4.71875 9H4.1875C1.875 9 0 10.9062 0 13.2188V14.5C0 15.3438 0.65625 16 1.5 16H12.5C13.3125 16 14 15.3438 14 14.5V13.2188C14 10.9062 12.0938 9 9.78125 9ZM19.875 5L19 4.125C18.875 3.96875 18.625 3.96875 18.5 4.125L15.2188 7.375L13.7812 5.9375C13.6562 5.78125 13.4062 5.78125 13.25 5.9375L12.375 6.8125C12.25 6.9375 12.25 7.1875 12.375 7.34375L14.9375 9.90625C15.0938 10.0625 15.3125 10.0625 15.4688 9.90625L19.875 5.53125C20.0312 5.375 20.0312 5.15625 19.875 5Z" fill="#6A737D"/>
</svg>

After

Width:  |  Height:  |  Size: 769 B

@ -1,7 +1,7 @@
{
"name": "__MSG_appName__",
"short_name": "__MSG_appName__",
"version": "7.4.0",
"version": "7.5.0",
"manifest_version": 2,
"author": "https://metamask.io",
"description": "__MSG_appDescription__",

@ -0,0 +1,25 @@
const EthJsEns = require('ethjs-ens')
const ensNetworkMap = require('ethjs-ens/lib/network-map.json')
class Ens {
static getNetworkEnsSupport (network) {
return Boolean(ensNetworkMap[network])
}
constructor ({ network, provider } = {}) {
this._ethJsEns = new EthJsEns({
network,
provider,
})
}
lookup (ensName) {
return this._ethJsEns.lookup(ensName)
}
reverse (address) {
return this._ethJsEns.reverse(address)
}
}
module.exports = Ens

@ -0,0 +1,94 @@
const ethUtil = require('ethereumjs-util')
const ObservableStore = require('obs-store')
const punycode = require('punycode')
const log = require('loglevel')
const Ens = require('./ens')
const ZERO_ADDRESS = '0x0000000000000000000000000000000000000000'
const ZERO_X_ERROR_ADDRESS = '0x'
class EnsController {
constructor ({ ens, provider, networkStore } = {}) {
const initState = {
ensResolutionsByAddress: {},
}
this._ens = ens
if (!this._ens) {
const network = networkStore.getState()
if (Ens.getNetworkEnsSupport(network)) {
this._ens = new Ens({
network,
provider,
})
}
}
this.store = new ObservableStore(initState)
networkStore.subscribe((network) => {
this.store.putState(initState)
if (Ens.getNetworkEnsSupport(network)) {
this._ens = new Ens({
network,
provider,
})
} else {
delete this._ens
}
})
}
reverseResolveAddress (address) {
return this._reverseResolveAddress(ethUtil.toChecksumAddress(address))
}
async _reverseResolveAddress (address) {
if (!this._ens) {
return undefined
}
const state = this.store.getState()
if (state.ensResolutionsByAddress[address]) {
return state.ensResolutionsByAddress[address]
}
let domain
try {
domain = await this._ens.reverse(address)
} catch (error) {
log.debug(error)
return undefined
}
let registeredAddress
try {
registeredAddress = await this._ens.lookup(domain)
} catch (error) {
log.debug(error)
return undefined
}
if (registeredAddress === ZERO_ADDRESS || registeredAddress === ZERO_X_ERROR_ADDRESS) {
return undefined
}
if (ethUtil.toChecksumAddress(registeredAddress) !== address) {
return undefined
}
this._updateResolutionsByAddress(address, punycode.toASCII(domain))
return domain
}
_updateResolutionsByAddress (address, domain) {
const oldState = this.store.getState()
this.store.putState({
ensResolutionsByAddress: {
...oldState.ensResolutionsByAddress,
[address]: domain,
},
})
}
}
module.exports = EnsController

@ -1,8 +1,7 @@
const mergeMiddleware = require('json-rpc-engine/src/mergeMiddleware')
const createScaffoldMiddleware = require('json-rpc-engine/src/createScaffoldMiddleware')
const createAsyncMiddleware = require('json-rpc-engine/src/createAsyncMiddleware')
const createWalletSubprovider = require('eth-json-rpc-middleware/wallet')
const { createPendingNonceMiddleware, createPendingTxMiddleware } = require('./middleware/pending')
module.exports = createMetamaskMiddleware
function createMetamaskMiddleware ({
@ -15,6 +14,7 @@ function createMetamaskMiddleware ({
processTypedMessageV4,
processPersonalMessage,
getPendingNonce,
getPendingTransactionByHash,
}) {
const metamaskMiddleware = mergeMiddleware([
createScaffoldMiddleware({
@ -32,16 +32,7 @@ function createMetamaskMiddleware ({
processPersonalMessage,
}),
createPendingNonceMiddleware({ getPendingNonce }),
createPendingTxMiddleware({ getPendingTransactionByHash }),
])
return metamaskMiddleware
}
function createPendingNonceMiddleware ({ getPendingNonce }) {
return createAsyncMiddleware(async (req, res, next) => {
if (req.method !== 'eth_getTransactionCount') return next()
const address = req.params[0]
const blockRef = req.params[1]
if (blockRef !== 'pending') return next()
res.result = await getPendingNonce(address)
})
}

@ -0,0 +1,28 @@
const { formatTxMetaForRpcResult } = require('../util')
const createAsyncMiddleware = require('json-rpc-engine/src/createAsyncMiddleware')
function createPendingNonceMiddleware ({ getPendingNonce }) {
return createAsyncMiddleware(async (req, res, next) => {
const {method, params} = req
if (method !== 'eth_getTransactionCount') return next()
const [param, blockRef] = params
if (blockRef !== 'pending') return next()
res.result = await getPendingNonce(param)
})
}
function createPendingTxMiddleware ({ getPendingTransactionByHash }) {
return createAsyncMiddleware(async (req, res, next) => {
const {method, params} = req
if (method !== 'eth_getTransactionByHash') return next()
const [hash] = params
const txMeta = getPendingTransactionByHash(hash)
if (!txMeta) return next()
res.result = formatTxMetaForRpcResult(txMeta)
})
}
module.exports = {
createPendingTxMiddleware,
createPendingNonceMiddleware,
}

@ -29,6 +29,27 @@ const networkToNameMap = {
const getNetworkDisplayName = key => networkToNameMap[key]
function formatTxMetaForRpcResult (txMeta) {
return {
'blockHash': txMeta.txReceipt ? txMeta.txReceipt.blockHash : null,
'blockNumber': txMeta.txReceipt ? txMeta.txReceipt.blockNumber : null,
'from': txMeta.txParams.from,
'gas': txMeta.txParams.gas,
'gasPrice': txMeta.txParams.gasPrice,
'hash': txMeta.hash,
'input': txMeta.txParams.data || '0x',
'nonce': txMeta.txParams.nonce,
'to': txMeta.txParams.to,
'transactionIndex': txMeta.txReceipt ? txMeta.txReceipt.transactionIndex : null,
'value': txMeta.txParams.value || '0x0',
'v': txMeta.v,
'r': txMeta.r,
's': txMeta.s,
}
}
module.exports = {
getNetworkDisplayName,
formatTxMetaForRpcResult,
}

@ -44,6 +44,7 @@ class PreferencesController {
// perform sensitive operations.
featureFlags: {
showIncomingTransactions: true,
transactionTime: false,
},
knownMethodData: {},
participateInMetaMetrics: null,

@ -423,6 +423,15 @@ class TransactionController extends EventEmitter {
const fromAddress = txParams.from
const ethTx = new Transaction(txParams)
await this.signEthTx(ethTx, fromAddress)
// add r,s,v values for provider request purposes see createMetamaskMiddleware
// and JSON rpc standard for further explanation
txMeta.r = ethUtil.bufferToHex(ethTx.r)
txMeta.s = ethUtil.bufferToHex(ethTx.s)
txMeta.v = ethUtil.bufferToHex(ethTx.v)
this.txStateManager.updateTx(txMeta, 'transactions#signTransaction: add r, s, v values')
// set state to signed
this.txStateManager.setTxStatusSigned(txMeta.id)
const rawTx = ethUtil.bufferToHex(ethTx.serialize())
@ -439,8 +448,19 @@ class TransactionController extends EventEmitter {
const txMeta = this.txStateManager.getTx(txId)
txMeta.rawTx = rawTx
this.txStateManager.updateTx(txMeta, 'transactions#publishTransaction')
const txHash = await this.query.sendRawTransaction(rawTx)
let txHash
try {
txHash = await this.query.sendRawTransaction(rawTx)
} catch (error) {
if (error.message.toLowerCase().includes('known transaction')) {
txHash = ethUtil.sha3(ethUtil.addHexPrefix(rawTx)).toString('hex')
txHash = ethUtil.addHexPrefix(txHash)
} else {
throw error
}
}
this.setTxHash(txId, txHash)
this.txStateManager.setTxStatusSubmitted(txId)
}

@ -174,7 +174,7 @@ class PendingTransactionTracker extends EventEmitter {
// get latest transaction status
try {
const { blockNumber } = await this.query.getTransactionByHash(txHash) || {}
const { blockNumber } = await this.query.getTransactionReceipt(txHash) || {}
if (blockNumber) {
this.emit('tx:confirmed', txId)
}
@ -196,7 +196,7 @@ class PendingTransactionTracker extends EventEmitter {
async _checkIftxWasDropped (txMeta) {
const { txParams: { nonce, from }, hash } = txMeta
const nextNonce = await this.query.getTransactionCount(from)
const { blockNumber } = await this.query.getTransactionByHash(hash) || {}
const { blockNumber } = await this.query.getTransactionReceipt(hash) || {}
if (!blockNumber && parseInt(nextNonce) > parseInt(nonce)) {
return true
}

@ -53,6 +53,8 @@ function setupEnsIpfsResolver ({ provider }) {
url = `https://swarm-gateways.net/bzz:/${hash}${path}${search || ''}`
} else if (type === 'onion' || type === 'onion3') {
url = `http://${hash}.onion${path}${search || ''}`
} else if (type === 'zeronet') {
url = `http://127.0.0.1:43110/${hash}${path}${search || ''}`
}
} catch (err) {
console.warn(err)

@ -23,6 +23,7 @@ const createLoggerMiddleware = require('./lib/createLoggerMiddleware')
const providerAsMiddleware = require('eth-json-rpc-middleware/providerAsMiddleware')
const {setupMultiplex} = require('./lib/stream-utils.js')
const KeyringController = require('eth-keyring-controller')
const EnsController = require('./controllers/ens')
const NetworkController = require('./controllers/network')
const PreferencesController = require('./controllers/preferences')
const AppStateController = require('./controllers/app-state')
@ -138,6 +139,11 @@ module.exports = class MetamaskController extends EventEmitter {
networkController: this.networkController,
})
this.ensController = new EnsController({
provider: this.provider,
networkStore: this.networkController.networkStore,
})
this.incomingTransactionsController = new IncomingTransactionsController({
blockTracker: this.blockTracker,
networkController: this.networkController,
@ -315,6 +321,8 @@ module.exports = class MetamaskController extends EventEmitter {
// ThreeBoxController
ThreeBoxController: this.threeBoxController.store,
ABTestController: this.abTestController.store,
// ENS Controller
EnsController: this.ensController.store,
})
this.memStore.subscribe(this.sendUpdate.bind(this))
}
@ -353,6 +361,7 @@ module.exports = class MetamaskController extends EventEmitter {
processTypedMessageV4: this.newUnsignedTypedMessage.bind(this),
processPersonalMessage: this.newUnsignedPersonalMessage.bind(this),
getPendingNonce: this.getPendingNonce.bind(this),
getPendingTransactionByHash: (hash) => this.txController.getFilteredTxList({ hash, status: 'submitted' })[0],
}
const providerProxy = this.networkController.initializeProvider(providerOpts)
return providerProxy
@ -500,6 +509,9 @@ module.exports = class MetamaskController extends EventEmitter {
// AppStateController
setLastActiveTime: nodeify(this.appStateController.setLastActiveTime, this.appStateController),
// EnsController
tryReverseResolveAddress: nodeify(this.ensController.reverseResolveAddress, this.ensController),
// KeyringController
setLocked: nodeify(this.setLocked, this),
createNewVaultAndKeychain: nodeify(this.createNewVaultAndKeychain, this),

@ -74,7 +74,7 @@
"c3": "^0.6.7",
"classnames": "^2.2.5",
"clone": "^2.1.2",
"content-hash": "^2.4.3",
"content-hash": "^2.4.4",
"copy-to-clipboard": "^3.0.8",
"currency-formatter": "^1.4.2",
"d3": "^5.7.0",
@ -103,7 +103,7 @@
"eth-trezor-keyring": "^0.4.0",
"ethereumjs-abi": "^0.6.4",
"ethereumjs-tx": "1.3.7",
"ethereumjs-util": "github:ethereumjs/ethereumjs-util#ac5d0908536b447083ea422b435da27f26615de9",
"ethereumjs-util": "5.1.0",
"ethereumjs-wallet": "^0.6.0",
"etherscan-link": "^1.0.2",
"ethjs": "^0.4.0",
@ -114,7 +114,7 @@
"extensionizer": "^1.0.1",
"fast-json-patch": "^2.0.4",
"fuse.js": "^3.2.0",
"gaba": "^1.7.5",
"gaba": "^1.8.0",
"human-standard-token-abi": "^2.0.0",
"jazzicon": "^1.2.0",
"json-rpc-engine": "^5.1.4",
@ -141,6 +141,7 @@
"prop-types": "^15.6.1",
"pubnub": "4.24.4",
"pump": "^3.0.0",
"punycode": "^2.1.1",
"qrcode-generator": "1.4.1",
"ramda": "^0.24.1",
"react": "^15.6.2",

@ -210,13 +210,13 @@ describe('MetaMask', function () {
})
it('finds the transaction in the transactions list', async function () {
const transactions = await findElements(driver, By.css('.transaction-list-item'))
assert.equal(transactions.length, 1)
await driver.wait(async () => {
const confirmedTxes = await findElements(driver, By.css('.transaction-list__completed-transactions .transaction-list-item'))
return confirmedTxes.length === 1
}, 10000)
if (process.env.SELENIUM_BROWSER !== 'firefox') {
const txValues = await findElement(driver, By.css('.transaction-list-item__amount--primary'))
await driver.wait(until.elementTextMatches(txValues, /-1\s*ETH/), 10000)
}
const txValues = await findElement(driver, By.css('.transaction-list-item__amount--primary'))
await driver.wait(until.elementTextMatches(txValues, /-1\s*ETH/), 10000)
})
})
@ -251,13 +251,13 @@ describe('MetaMask', function () {
})
it('finds the transaction in the transactions list', async function () {
const transactions = await findElements(driver, By.css('.transaction-list-item'))
assert.equal(transactions.length, 2)
await driver.wait(async () => {
const confirmedTxes = await findElements(driver, By.css('.transaction-list__completed-transactions .transaction-list-item'))
return confirmedTxes.length === 2
}, 10000)
if (process.env.SELENIUM_BROWSER !== 'firefox') {
const txValues = await findElement(driver, By.css('.transaction-list-item__amount--primary'))
await driver.wait(until.elementTextMatches(txValues, /-2\s*ETH/), 10000)
}
const txValues = await findElement(driver, By.css('.transaction-list-item__amount--primary'))
await driver.wait(until.elementTextMatches(txValues, /-2\s*ETH/), 10000)
})
})
})

@ -226,8 +226,10 @@ describe('Using MetaMask with an existing account', function () {
})
it('finds the transaction in the transactions list', async function () {
const transactions = await findElements(driver, By.css('.transaction-list-item'))
assert.equal(transactions.length, 1)
await driver.wait(async () => {
const confirmedTxes = await findElements(driver, By.css('.transaction-list__completed-transactions .transaction-list-item'))
return confirmedTxes.length === 1
}, 10000)
const txValues = await findElements(driver, By.css('.transaction-list-item__amount--primary'))
assert.equal(txValues.length, 1)

@ -91,7 +91,7 @@ async function getExtensionIdChrome (driver) {
async function getExtensionIdFirefox (driver) {
await driver.get('about:debugging#addons')
const extensionId = await driver.findElement(By.css('dd.addon-target-info-content:nth-child(6) > span:nth-child(1)')).getText()
const extensionId = await driver.wait(webdriver.until.elementLocated(By.xpath('//dl/div[contains(., \'Internal UUID\')]/dd')), 1000).getText()
return extensionId
}

@ -231,13 +231,13 @@ describe('MetaMask', function () {
})
it('finds the transaction in the transactions list', async function () {
const transactions = await findElements(driver, By.css('.transaction-list-item'))
assert.equal(transactions.length, 1)
await driver.wait(async () => {
const confirmedTxes = await findElements(driver, By.css('.transaction-list__completed-transactions .transaction-list-item'))
return confirmedTxes.length === 1
}, 10000)
if (process.env.SELENIUM_BROWSER !== 'firefox') {
const txValues = await findElement(driver, By.css('.transaction-list-item__amount--primary'))
await driver.wait(until.elementTextMatches(txValues, /-1\s*ETH/), 10000)
}
const txValues = await findElement(driver, By.css('.transaction-list-item__amount--primary'))
await driver.wait(until.elementTextMatches(txValues, /-1\s*ETH/), 10000)
})
})
})

@ -289,13 +289,13 @@ describe('MetaMask', function () {
})
it('finds the transaction in the transactions list', async function () {
const transactions = await findElements(driver, By.css('.transaction-list-item'))
assert.equal(transactions.length, 1)
await driver.wait(async () => {
const confirmedTxes = await findElements(driver, By.css('.transaction-list__completed-transactions .transaction-list-item'))
return confirmedTxes.length === 1
}, 10000)
if (process.env.SELENIUM_BROWSER !== 'firefox') {
const txValues = await findElement(driver, By.css('.transaction-list-item__amount--primary'))
await driver.wait(until.elementTextMatches(txValues, /-1\s*ETH/), 10000)
}
const txValues = await findElement(driver, By.css('.transaction-list-item__amount--primary'))
await driver.wait(until.elementTextMatches(txValues, /-1\s*ETH/), 10000)
})
})
@ -332,13 +332,13 @@ describe('MetaMask', function () {
})
it('finds the transaction in the transactions list', async function () {
const transactions = await findElements(driver, By.css('.transaction-list-item'))
assert.equal(transactions.length, 2)
await driver.wait(async () => {
const confirmedTxes = await findElements(driver, By.css('.transaction-list__completed-transactions .transaction-list-item'))
return confirmedTxes.length === 2
}, 10000)
if (process.env.SELENIUM_BROWSER !== 'firefox') {
const txValues = await findElement(driver, By.css('.transaction-list-item__amount--primary'))
await driver.wait(until.elementTextMatches(txValues, /-1\s*ETH/), 10000)
}
const txValues = await findElement(driver, By.css('.transaction-list-item__amount--primary'))
await driver.wait(until.elementTextMatches(txValues, /-1\s*ETH/), 10000)
})
})
@ -385,13 +385,13 @@ describe('MetaMask', function () {
})
it('finds the transaction in the transactions list', async function () {
const transactions = await findElements(driver, By.css('.transaction-list-item'))
assert.equal(transactions.length, 3)
await driver.wait(async () => {
const confirmedTxes = await findElements(driver, By.css('.transaction-list__completed-transactions .transaction-list-item'))
return confirmedTxes.length === 3
}, 10000)
if (process.env.SELENIUM_BROWSER !== 'firefox') {
const txValues = await findElement(driver, By.css('.transaction-list-item__amount--primary'))
await driver.wait(until.elementTextMatches(txValues, /-1\s*ETH/), 10000)
}
const txValues = await findElement(driver, By.css('.transaction-list-item__amount--primary'))
await driver.wait(until.elementTextMatches(txValues, /-1\s*ETH/), 10000)
})
})
@ -838,12 +838,10 @@ describe('MetaMask', function () {
it('renders the correct ETH balance', async () => {
const balance = await findElement(driver, By.css('.transaction-view-balance__primary-balance'))
await delay(regularDelayMs)
if (process.env.SELENIUM_BROWSER !== 'firefox') {
await driver.wait(until.elementTextMatches(balance, /^87.*\s*ETH.*$/), 10000)
const tokenAmount = await balance.getText()
assert.ok(/^87.*\s*ETH.*$/.test(tokenAmount))
await delay(regularDelayMs)
}
await driver.wait(until.elementTextMatches(balance, /^87.*\s*ETH.*$/), 10000)
const tokenAmount = await balance.getText()
assert.ok(/^87.*\s*ETH.*$/.test(tokenAmount))
await delay(regularDelayMs)
})
})
@ -1002,22 +1000,15 @@ describe('MetaMask', function () {
})
it('finds the transaction in the transactions list', async function () {
const transactions = await findElements(driver, By.css('.transaction-list-item'))
assert.equal(transactions.length, 1)
const txValues = await findElements(driver, By.css('.transaction-list-item__amount--primary'))
assert.equal(txValues.length, 1)
// test cancelled on firefox until https://github.com/mozilla/geckodriver/issues/906 is resolved,
// or possibly until we use latest version of firefox in the tests
if (process.env.SELENIUM_BROWSER !== 'firefox') {
await driver.wait(until.elementTextMatches(txValues[0], /-1\s*TST/), 10000)
}
await driver.wait(async () => {
const confirmedTxes = await findElements(driver, By.css('.transaction-list__completed-transactions .transaction-list-item'))
return confirmedTxes.length === 1
}, 10000)
const txValues = await findElements(driver, By.css('.transaction-list-item__amount--primary'))
assert.equal(txValues.length, 1)
await driver.wait(until.elementTextMatches(txValues[0], /-1\s*TST/), 10000)
const txStatuses = await findElements(driver, By.css('.transaction-list-item__action'))
await driver.wait(until.elementTextMatches(txStatuses[0], /Sent\sToken/i), 10000)
})
@ -1104,7 +1095,6 @@ describe('MetaMask', function () {
return confirmedTxes.length === 2
}, 10000)
await delay(regularDelayMs)
const txValues = await findElements(driver, By.css('.transaction-list-item__amount--primary'))
await driver.wait(until.elementTextMatches(txValues[0], /-1.5\s*TST/))
const txStatuses = await findElements(driver, By.css('.transaction-list-item__action'))
@ -1115,14 +1105,10 @@ describe('MetaMask', function () {
const tokenListItems = await findElements(driver, By.css('.token-list-item'))
await tokenListItems[0].click()
await delay(regularDelayMs)
await delay(1000)
// test cancelled on firefox until https://github.com/mozilla/geckodriver/issues/906 is resolved,
// or possibly until we use latest version of firefox in the tests
if (process.env.SELENIUM_BROWSER !== 'firefox') {
const tokenBalanceAmount = await findElements(driver, By.css('.transaction-view-balance__primary-balance'))
await driver.wait(until.elementTextMatches(tokenBalanceAmount[0], /7.500\s*TST/), 10000)
}
const tokenBalanceAmount = await findElements(driver, By.css('.transaction-view-balance__primary-balance'))
await driver.wait(until.elementTextMatches(tokenBalanceAmount[0], /7.500\s*TST/), 10000)
})
})
@ -1138,12 +1124,9 @@ describe('MetaMask', function () {
await driver.switchTo().window(dapp)
await delay(tinyDelayMs)
const transferTokens = await findElement(driver, By.xpath(`//button[contains(text(), 'Approve Tokens')]`))
await transferTokens.click()
const approveTokens = await findElement(driver, By.xpath(`//button[contains(text(), 'Approve Tokens')]`))
await approveTokens.click()
if (process.env.SELENIUM_BROWSER !== 'firefox') {
await closeAllWindowHandlesExcept(driver, [extension, dapp])
}
await driver.switchTo().window(extension)
await delay(regularDelayMs)
@ -1160,31 +1143,22 @@ describe('MetaMask', function () {
})
it('displays the token approval data', async () => {
const dataTab = await findElement(driver, By.xpath(`//li[contains(text(), 'Data')]`))
dataTab.click()
const fullTxDataButton = await findElement(driver, By.css('.confirm-approve-content__view-full-tx-button'))
await fullTxDataButton.click()
await delay(regularDelayMs)
const functionType = await findElement(driver, By.css('.confirm-page-container-content__function-type'))
const functionType = await findElement(driver, By.css('.confirm-approve-content__data .confirm-approve-content__small-text'))
const functionTypeText = await functionType.getText()
assert.equal(functionTypeText, 'Approve')
assert.equal(functionTypeText, 'Function: Approve')
const confirmDataDiv = await findElement(driver, By.css('.confirm-page-container-content__data-box'))
const confirmDataDiv = await findElement(driver, By.css('.confirm-approve-content__data__data-block'))
const confirmDataText = await confirmDataDiv.getText()
assert(confirmDataText.match(/0x095ea7b30000000000000000000000009bc5baf874d2da8d216ae9f137804184ee5afef4/))
const detailsTab = await findElement(driver, By.xpath(`//li[contains(text(), 'Details')]`))
detailsTab.click()
await delay(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 delay(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()
const editButtons = await findElements(driver, By.css('.confirm-approve-content__small-blue-text.cursor-pointer'))
await editButtons[0].click()
await delay(regularDelayMs)
gasModal = await driver.findElement(By.css('span .modal'))
@ -1215,14 +1189,34 @@ describe('MetaMask', function () {
await save.click()
await driver.wait(until.stalenessOf(gasModal))
const gasFeeInputs = await findElements(driver, By.css('.confirm-detail-row__primary'))
assert.equal(await gasFeeInputs[0].getText(), '0.0006')
const gasFeeInEth = await findElement(driver, By.css('.confirm-approve-content__transaction-details-content__secondary-fee'))
assert.equal(await gasFeeInEth.getText(), '0.0006')
})
it('shows the correct recipient', async function () {
const senderToRecipientDivs = await findElements(driver, By.css('.sender-to-recipient__name'))
const recipientDiv = senderToRecipientDivs[1]
assert.equal(await recipientDiv.getText(), '0x9bc5...fEF4')
it('edits the permission', async () => {
const editButtons = await findElements(driver, By.css('.confirm-approve-content__small-blue-text.cursor-pointer'))
await editButtons[1].click()
await delay(regularDelayMs)
const permissionModal = await driver.findElement(By.css('span .modal'))
const radioButtons = await findElements(driver, By.css('.edit-approval-permission__edit-section__radio-button'))
await radioButtons[1].click()
const customInput = await findElement(driver, By.css('input'))
await delay(50)
await customInput.sendKeys('5')
await delay(regularDelayMs)
const saveButton = await findElement(driver, By.xpath(`//button[contains(text(), 'Save')]`))
await saveButton.click()
await delay(regularDelayMs)
await driver.wait(until.stalenessOf(permissionModal))
const permissionInfo = await findElements(driver, By.css('.confirm-approve-content__medium-text'))
const amountDiv = permissionInfo[0]
assert.equal(await amountDiv.getText(), '5 TST')
})
it('submits the transaction', async function () {
@ -1232,29 +1226,19 @@ describe('MetaMask', function () {
})
it('finds the transaction in the transactions list', async function () {
if (process.env.SELENIUM_BROWSER === 'firefox') {
this.skip()
}
await driver.wait(async () => {
const confirmedTxes = await findElements(driver, By.css('.transaction-list__completed-transactions .transaction-list-item'))
return confirmedTxes.length === 3
}, 10000)
const txValues = await findElements(driver, By.css('.transaction-list-item__amount--primary'))
await driver.wait(until.elementTextMatches(txValues[0], /-7\s*TST/))
await driver.wait(until.elementTextMatches(txValues[0], /-5\s*TST/))
const txStatuses = await findElements(driver, By.css('.transaction-list-item__action'))
await driver.wait(until.elementTextMatches(txStatuses[0], /Approve/))
})
})
describe('Tranfers a custom token from dapp when no gas value is specified', () => {
before(function () {
if (process.env.SELENIUM_BROWSER === 'firefox') {
this.skip()
}
})
it('transfers an already created token, without specifying gas', async () => {
const windowHandles = await driver.getAllWindowHandles()
const extension = windowHandles[0]
@ -1267,7 +1251,6 @@ describe('MetaMask', function () {
const transferTokens = await findElement(driver, By.xpath(`//button[contains(text(), 'Transfer Tokens Without Gas')]`))
await transferTokens.click()
await closeAllWindowHandlesExcept(driver, [extension, dapp])
await driver.switchTo().window(extension)
await delay(regularDelayMs)
@ -1304,12 +1287,6 @@ describe('MetaMask', function () {
})
describe('Approves a custom token from dapp when no gas value is specified', () => {
before(function () {
if (process.env.SELENIUM_BROWSER === 'firefox') {
this.skip()
}
})
it('approves an already created token', async () => {
const windowHandles = await driver.getAllWindowHandles()
const extension = windowHandles[0]
@ -1323,7 +1300,6 @@ describe('MetaMask', function () {
const transferTokens = await findElement(driver, By.xpath(`//button[contains(text(), 'Approve Tokens Without Gas')]`))
await transferTokens.click()
await closeAllWindowHandlesExcept(driver, extension)
await driver.switchTo().window(extension)
await delay(regularDelayMs)
@ -1340,13 +1316,17 @@ describe('MetaMask', function () {
})
it('shows the correct recipient', async function () {
const senderToRecipientDivs = await findElements(driver, By.css('.sender-to-recipient__name'))
const recipientDiv = senderToRecipientDivs[1]
assert.equal(await recipientDiv.getText(), 'Account 2')
const fullTxDataButton = await findElement(driver, By.css('.confirm-approve-content__view-full-tx-button'))
await fullTxDataButton.click()
await delay(regularDelayMs)
const permissionInfo = await findElements(driver, By.css('.confirm-approve-content__medium-text'))
const recipientDiv = permissionInfo[1]
assert.equal(await recipientDiv.getText(), '0x2f318C33...C970')
})
it('submits the transaction', async function () {
await delay(regularDelayMs)
await delay(1000)
const confirmButton = await findElement(driver, By.xpath(`//button[contains(text(), 'Confirm')]`))
await confirmButton.click()
await delay(regularDelayMs)

@ -37,7 +37,6 @@ concurrently --kill-others \
'yarn ganache:start' \
'sleep 5 && mocha test/e2e/from-import-ui.spec'
export GANACHE_ARGS="${BASE_GANACHE_ARGS} --deterministic --account=0x53CB0AB5226EEBF4D872113D98332C1555DC304443BEE1CF759D15798D3C55A9,25000000000000000000"
concurrently --kill-others \
--names 'ganache,e2e' \
--prefix '[{time}][{name}]' \
@ -45,14 +44,13 @@ concurrently --kill-others \
'npm run ganache:start' \
'sleep 5 && mocha test/e2e/send-edit.spec'
concurrently --kill-others \
--names 'ganache,dapp,e2e' \
--prefix '[{time}][{name}]' \
--success first \
'yarn ganache:start' \
'yarn dapp' \
'sleep 5 && mocha test/e2e/ethereum-on.spec'
concurrently --kill-others \
--names 'ganache,dapp,e2e' \
--prefix '[{time}][{name}]' \
--success first \
'yarn ganache:start' \
'yarn dapp' \
'sleep 5 && mocha test/e2e/ethereum-on.spec'
export GANACHE_ARGS="${BASE_GANACHE_ARGS} --deterministic --account=0x250F458997A364988956409A164BA4E16F0F99F916ACDD73ADCD3A1DE30CF8D1,0 --account=0x53CB0AB5226EEBF4D872113D98332C1555DC304443BEE1CF759D15798D3C55A9,25000000000000000000"
concurrently --kill-others \
@ -73,12 +71,11 @@ concurrently --kill-others \
'sleep 5 && mocha test/e2e/address-book.spec'
export GANACHE_ARGS="${BASE_GANACHE_ARGS} --deterministic --account=0x53CB0AB5226EEBF4D872113D98332C1555DC304443BEE1CF759D15798D3C55A9,25000000000000000000"
concurrently --kill-others \
--names 'ganache,dapp,e2e' \
--prefix '[{time}][{name}]' \
--success first \
'node test/e2e/mock-3box/server.js' \
'yarn ganache:start' \
'yarn dapp' \
'sleep 5 && mocha test/e2e/threebox.spec'
concurrently --kill-others \
--names 'ganache,dapp,e2e' \
--prefix '[{time}][{name}]' \
--success first \
'node test/e2e/mock-3box/server.js' \
'yarn ganache:start' \
'yarn dapp' \
'sleep 5 && mocha test/e2e/threebox.spec'

@ -218,8 +218,10 @@ describe('Using MetaMask with an existing account', function () {
})
it('finds the transaction in the transactions list', async function () {
const transactions = await findElements(driver, By.css('.transaction-list-item'))
assert.equal(transactions.length, 1)
await driver.wait(async () => {
const confirmedTxes = await findElements(driver, By.css('.transaction-list__completed-transactions .transaction-list-item'))
return confirmedTxes.length === 1
}, 10000)
const txValues = await findElements(driver, By.css('.transaction-list-item__amount--primary'))
assert.equal(txValues.length, 1)

@ -0,0 +1,135 @@
const assert = require('assert')
const sinon = require('sinon')
const ObservableStore = require('obs-store')
const HttpProvider = require('ethjs-provider-http')
const EnsController = require('../../../../app/scripts/controllers/ens')
const ZERO_ADDRESS = '0x0000000000000000000000000000000000000000'
const ZERO_X_ERROR_ADDRESS = '0x'
describe('EnsController', function () {
describe('#constructor', function () {
it('should construct the controller given a provider and a network', async () => {
const provider = new HttpProvider('https://ropsten.infura.io')
const currentNetworkId = '3'
const networkStore = new ObservableStore(currentNetworkId)
const ens = new EnsController({
provider,
networkStore,
})
assert.ok(ens._ens)
})
it('should construct the controller given an existing ENS instance', async () => {
const networkStore = {
subscribe: sinon.spy(),
}
const ens = new EnsController({
ens: {},
networkStore,
})
assert.ok(ens._ens)
})
})
describe('#reverseResolveName', function () {
it('should resolve to an ENS name', async () => {
const address = '0x8e5d75d60224ea0c33d0041e75de68b1c3cb6dd5'
const networkStore = {
subscribe: sinon.spy(),
}
const ens = new EnsController({
ens: {
reverse: sinon.stub().withArgs(address).returns('peaksignal.eth'),
lookup: sinon.stub().withArgs('peaksignal.eth').returns(address),
},
networkStore,
})
const name = await ens.reverseResolveAddress(address)
assert.equal(name, 'peaksignal.eth')
})
it('should only resolve an ENS name once', async () => {
const address = '0x8e5d75d60224ea0c33d0041e75de68b1c3cb6dd5'
const reverse = sinon.stub().withArgs(address).returns('peaksignal.eth')
const lookup = sinon.stub().withArgs('peaksignal.eth').returns(address)
const networkStore = {
subscribe: sinon.spy(),
}
const ens = new EnsController({
ens: {
reverse,
lookup,
},
networkStore,
})
assert.equal(await ens.reverseResolveAddress(address), 'peaksignal.eth')
assert.equal(await ens.reverseResolveAddress(address), 'peaksignal.eth')
assert.ok(lookup.calledOnce)
assert.ok(reverse.calledOnce)
})
it('should fail if the name is registered to a different address than the reverse-resolved', async () => {
const address = '0x8e5d75d60224ea0c33d0041e75de68b1c3cb6dd5'
const networkStore = {
subscribe: sinon.spy(),
}
const ens = new EnsController({
ens: {
reverse: sinon.stub().withArgs(address).returns('peaksignal.eth'),
lookup: sinon.stub().withArgs('peaksignal.eth').returns('0xfoo'),
},
networkStore,
})
const name = await ens.reverseResolveAddress(address)
assert.strictEqual(name, undefined)
})
it('should throw an error when the lookup resolves to the zero address', async () => {
const address = '0x8e5d75d60224ea0c33d0041e75de68b1c3cb6dd5'
const networkStore = {
subscribe: sinon.spy(),
}
const ens = new EnsController({
ens: {
reverse: sinon.stub().withArgs(address).returns('peaksignal.eth'),
lookup: sinon.stub().withArgs('peaksignal.eth').returns(ZERO_ADDRESS),
},
networkStore,
})
try {
await ens.reverseResolveAddress(address)
assert.fail('#reverseResolveAddress did not throw')
} catch (e) {
assert.ok(e)
}
})
it('should throw an error the lookup resolves to the zero x address', async () => {
const address = '0x8e5d75d60224ea0c33d0041e75de68b1c3cb6dd5'
const networkStore = {
subscribe: sinon.spy(),
}
const ens = new EnsController({
ens: {
reverse: sinon.stub().withArgs(address).returns('peaksignal.eth'),
lookup: sinon.stub().withArgs('peaksignal.eth').returns(ZERO_X_ERROR_ADDRESS),
},
networkStore,
})
try {
await ens.reverseResolveAddress(address)
assert.fail('#reverseResolveAddress did not throw')
} catch (e) {
assert.ok(e)
}
})
})
})

@ -1,9 +1,9 @@
const assert = require('assert')
const nock = require('nock')
const NetworkController = require('../../../../app/scripts/controllers/network')
const NetworkController = require('../../../../../app/scripts/controllers/network')
const {
getNetworkDisplayName,
} = require('../../../../app/scripts/controllers/network/util')
} = require('../../../../../app/scripts/controllers/network/util')
describe('# Network Controller', function () {
let networkController

@ -0,0 +1,81 @@
const assert = require('assert')
const { createPendingNonceMiddleware, createPendingTxMiddleware } = require('../../../../../app/scripts/controllers/network/middleware/pending')
const txMetaStub = require('./stubs').txMetaStub
describe('#createPendingNonceMiddleware', function () {
const getPendingNonce = async () => '0x2'
const address = '0xF231D46dD78806E1DD93442cf33C7671f8538748'
const pendingNonceMiddleware = createPendingNonceMiddleware({ getPendingNonce })
it('should call next if not a eth_getTransactionCount request', (done) => {
const req = {method: 'eth_getBlockByNumber'}
const res = {}
pendingNonceMiddleware(req, res, () => done())
})
it('should call next if not a "pending" block request', (done) => {
const req = { method: 'eth_getTransactionCount', params: [address] }
const res = {}
pendingNonceMiddleware(req, res, () => done())
})
it('should fill the result with a the "pending" nonce', (done) => {
const req = { method: 'eth_getTransactionCount', params: [address, 'pending'] }
const res = {}
pendingNonceMiddleware(req, res, () => { done(new Error('should not have called next')) }, () => {
assert(res.result === '0x2')
done()
})
})
})
describe('#createPendingTxMiddleware', function () {
let returnUndefined = true
const getPendingTransactionByHash = () => returnUndefined ? undefined : txMetaStub
const address = '0xF231D46dD78806E1DD93442cf33C7671f8538748'
const pendingTxMiddleware = createPendingTxMiddleware({ getPendingTransactionByHash })
const spec = {
'blockHash': null,
'blockNumber': null,
'from': '0xf231d46dd78806e1dd93442cf33c7671f8538748',
'gas': '0x5208',
'gasPrice': '0x1e8480',
'hash': '0x2cc5a25744486f7383edebbf32003e5a66e18135799593d6b5cdd2bb43674f09',
'input': '0x',
'nonce': '0x4',
'to': '0xf231d46dd78806e1dd93442cf33c7671f8538748',
'transactionIndex': null,
'value': '0x0',
'v': '0x2c',
'r': '0x5f973e540f2d3c2f06d3725a626b75247593cb36477187ae07ecfe0a4db3cf57',
's': '0x0259b52ee8c58baaa385fb05c3f96116e58de89bcc165cb3bfdfc708672fed8a',
}
it('should call next if not a eth_getTransactionByHash request', (done) => {
const req = {method: 'eth_getBlockByNumber'}
const res = {}
pendingTxMiddleware(req, res, () => done())
})
it('should call next if no pending txMeta is in history', (done) => {
const req = { method: 'eth_getTransactionByHash', params: [address] }
const res = {}
pendingTxMiddleware(req, res, () => done())
})
it('should fill the result with a the "pending" tx the result should match the rpc spec', (done) => {
returnUndefined = false
const req = { method: 'eth_getTransactionByHash', params: [address, 'pending'] }
const res = {}
pendingTxMiddleware(req, res, () => { done(new Error('should not have called next')) }, () => {
/*
// uncomment this section for debugging help with non matching keys
const coppy = {...res.result}
Object.keys(spec).forEach((key) => {
console.log(coppy[key], '===', spec[key], coppy[key] === spec[key], key)
delete coppy[key]
})
console.log(coppy)
*/
assert.deepStrictEqual(res.result, spec, new Error('result does not match the spec object'))
done()
})
})
})

@ -0,0 +1,225 @@
/*
this file is for all my big stubs because i don't want to
to mingle with my tests
*/
module.exports = {}
// for pending middlewares test
module.exports.txMetaStub = {
'estimatedGas': '0x5208',
'firstRetryBlockNumber': '0x51a402',
'gasLimitSpecified': true,
'gasPriceSpecified': true,
'hash': '0x2cc5a25744486f7383edebbf32003e5a66e18135799593d6b5cdd2bb43674f09',
'history': [
{
'id': 405984854664302,
'loadingDefaults': true,
'metamaskNetworkId': '4',
'status': 'unapproved',
'time': 1572395156620,
'transactionCategory': 'sentEther',
'txParams': {
'from': '0xf231d46dd78806e1dd93442cf33c7671f8538748',
'gas': '0x5208',
'gasPrice': '0x1e8480',
'to': '0xf231d46dd78806e1dd93442cf33c7671f8538748',
'value': '0x0',
},
'type': 'standard',
},
[
{
'op': 'replace',
'path': '/loadingDefaults',
'timestamp': 1572395156645,
'value': false,
},
{
'op': 'add',
'path': '/gasPriceSpecified',
'value': true,
},
{
'op': 'add',
'path': '/gasLimitSpecified',
'value': true,
},
{
'op': 'add',
'path': '/estimatedGas',
'value': '0x5208',
},
],
[
{
'note': '#newUnapprovedTransaction - adding the origin',
'op': 'add',
'path': '/origin',
'timestamp': 1572395156645,
'value': 'MetaMask',
},
],
[],
[
{
'note': 'txStateManager: setting status to approved',
'op': 'replace',
'path': '/status',
'timestamp': 1572395158240,
'value': 'approved',
},
],
[
{
'note': 'transactions#approveTransaction',
'op': 'add',
'path': '/txParams/nonce',
'timestamp': 1572395158261,
'value': '0x4',
},
{
'op': 'add',
'path': '/nonceDetails',
'value': {
'local': {
'details': {
'highest': 4,
'startPoint': 4,
},
'name': 'local',
'nonce': 4,
},
'network': {
'details': {
'baseCount': 4,
'blockNumber': '0x51a401',
},
'name': 'network',
'nonce': 4,
},
'params': {
'highestLocallyConfirmed': 0,
'highestSuggested': 4,
'nextNetworkNonce': 4,
},
},
},
],
[
{
'note': 'transactions#signTransaction: add r, s, v values',
'op': 'add',
'path': '/r',
'timestamp': 1572395158280,
'value': '0x5f973e540f2d3c2f06d3725a626b75247593cb36477187ae07ecfe0a4db3cf57',
},
{
'op': 'add',
'path': '/s',
'value': '0x0259b52ee8c58baaa385fb05c3f96116e58de89bcc165cb3bfdfc708672fed8a',
},
{
'op': 'add',
'path': '/v',
'value': '0x2c',
},
],
[
{
'note': 'transactions#publishTransaction',
'op': 'replace',
'path': '/status',
'timestamp': 1572395158281,
'value': 'signed',
},
{
'op': 'add',
'path': '/rawTx',
'value': '0xf86204831e848082520894f231d46dd78806e1dd93442cf33c7671f853874880802ca05f973e540f2d3c2f06d3725a626b75247593cb36477187ae07ecfe0a4db3cf57a00259b52ee8c58baaa385fb05c3f96116e58de89bcc165cb3bfdfc708672fed8a',
},
],
[],
[
{
'note': 'transactions#setTxHash',
'op': 'add',
'path': '/hash',
'timestamp': 1572395158570,
'value': '0x2cc5a25744486f7383edebbf32003e5a66e18135799593d6b5cdd2bb43674f09',
},
],
[
{
'note': 'txStateManager - add submitted time stamp',
'op': 'add',
'path': '/submittedTime',
'timestamp': 1572395158571,
'value': 1572395158570,
},
],
[
{
'note': 'txStateManager: setting status to submitted',
'op': 'replace',
'path': '/status',
'timestamp': 1572395158576,
'value': 'submitted',
},
],
[
{
'note': 'transactions/pending-tx-tracker#event: tx:block-update',
'op': 'add',
'path': '/firstRetryBlockNumber',
'timestamp': 1572395168972,
'value': '0x51a402',
},
],
],
'id': 405984854664302,
'loadingDefaults': false,
'metamaskNetworkId': '4',
'nonceDetails': {
'local': {
'details': {
'highest': 4,
'startPoint': 4,
},
'name': 'local',
'nonce': 4,
},
'network': {
'details': {
'baseCount': 4,
'blockNumber': '0x51a401',
},
'name': 'network',
'nonce': 4,
},
'params': {
'highestLocallyConfirmed': 0,
'highestSuggested': 4,
'nextNetworkNonce': 4,
},
},
'origin': 'MetaMask',
'r': '0x5f973e540f2d3c2f06d3725a626b75247593cb36477187ae07ecfe0a4db3cf57',
'rawTx': '0xf86204831e848082520894f231d46dd78806e1dd93442cf33c7671f853874880802ca05f973e540f2d3c2f06d3725a626b75247593cb36477187ae07ecfe0a4db3cf57a00259b52ee8c58baaa385fb05c3f96116e58de89bcc165cb3bfdfc708672fed8a',
's': '0x0259b52ee8c58baaa385fb05c3f96116e58de89bcc165cb3bfdfc708672fed8a',
'status': 'submitted',
'submittedTime': 1572395158570,
'time': 1572395156620,
'transactionCategory': 'sentEther',
'txParams': {
'from': '0xf231d46dd78806e1dd93442cf33c7671f8538748',
'gas': '0x5208',
'gasPrice': '0x1e8480',
'nonce': '0x4',
'to': '0xf231d46dd78806e1dd93442cf33c7671f8538748',
'value': '0x0',
},
'type': 'standard',
'v': '0x2c',
}

@ -496,6 +496,16 @@ describe('Transaction Controller', function () {
assert.equal(publishedTx.hash, hash)
assert.equal(publishedTx.status, 'submitted')
})
it('should ignore the error "Transaction Failed: known transaction" and be as usual', async function () {
providerResultStub['eth_sendRawTransaction'] = async (_, __, ___, end) => { end('Transaction Failed: known transaction') }
const rawTx = '0xf86204831e848082520894f231d46dd78806e1dd93442cf33c7671f853874880802ca05f973e540f2d3c2f06d3725a626b75247593cb36477187ae07ecfe0a4db3cf57a00259b52ee8c58baaa385fb05c3f96116e58de89bcc165cb3bfdfc708672fed8a'
txController.txStateManager.addTx(txMeta)
await txController.publishTransaction(txMeta.id, rawTx)
const publishedTx = txController.txStateManager.getTx(1)
assert.equal(publishedTx.hash, '0x2cc5a25744486f7383edebbf32003e5a66e18135799593d6b5cdd2bb43674f09')
assert.equal(publishedTx.status, 'submitted')
})
})
describe('#retryTransaction', function () {

@ -109,12 +109,6 @@ describe('Selectors', function () {
assert.equal(currentAccountwithSendEther.name, 'Test Account')
})
describe('#transactionSelector', function () {
it('returns transactions from state', function () {
selectors.transactionsSelector(mockState)
})
})
it('#getGasIsLoading', () => {
const gasIsLoading = selectors.getGasIsLoading(mockState)
assert.equal(gasIsLoading, false)

@ -2,7 +2,7 @@
position: fixed;
z-index: 100;
top: 58px;
width: 310px;
width: 320px;
@media screen and (max-width: 575px) {
right: calc(((100vw - 100%) / 2) + 8px);
@ -58,6 +58,7 @@
max-height: 256px;
position: relative;
z-index: 200;
scrollbar-width: none;
&::-webkit-scrollbar {
display: none;

@ -4,6 +4,7 @@
.confirm-page-container-content {
overflow-y: auto;
height: 100%;
flex: 1;
&__error-container {

@ -5,20 +5,24 @@ import {
ENVIRONMENT_TYPE_NOTIFICATION,
} from '../../../../../../app/scripts/lib/enums'
import NetworkDisplay from '../../network-display'
import Identicon from '../../../ui/identicon'
import { addressSlicer } from '../../../../helpers/utils/util'
export default class ConfirmPageContainer extends Component {
export default class ConfirmPageContainerHeader extends Component {
static contextTypes = {
t: PropTypes.func,
}
static propTypes = {
accountAddress: PropTypes.string,
showAccountInHeader: PropTypes.bool,
showEdit: PropTypes.bool,
onEdit: PropTypes.func,
children: PropTypes.node,
}
renderTop () {
const { onEdit, showEdit } = this.props
const { onEdit, showEdit, accountAddress, showAccountInHeader } = this.props
const windowType = window.METAMASK_UI_TYPE
const isFullScreen = windowType !== ENVIRONMENT_TYPE_NOTIFICATION &&
windowType !== ENVIRONMENT_TYPE_POPUP
@ -29,22 +33,39 @@ export default class ConfirmPageContainer extends Component {
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()}
{ !showAccountInHeader
? <div
className="confirm-page-container-header__back-button-container"
style={{
visibility: showEdit ? 'initial' : 'hidden',
}}
>
{ this.context.t('edit') }
</span>
</div>
<img
src="/images/caret-left.svg"
/>
<span
className="confirm-page-container-header__back-button"
onClick={() => onEdit()}
>
{ this.context.t('edit') }
</span>
</div>
: null
}
{ showAccountInHeader
? <div className="confirm-page-container-header__address-container">
<div className="confirm-page-container-header__address-identicon">
<Identicon
address={accountAddress}
diameter={24}
/>
</div>
<div className="confirm-page-container-header__address">
{ addressSlicer(accountAddress) }
</div>
</div>
: null
}
{ !isFullScreen && <NetworkDisplay /> }
</div>
)

@ -9,6 +9,7 @@
border-bottom: 1px solid $geyser;
padding: 4px 13px 4px 13px;
flex: 0 0 auto;
align-items: center;
}
&__back-button-container {
@ -28,4 +29,16 @@
font-weight: 400;
padding-left: 5px;
}
&__address-container {
display: flex;
align-items: center;
margin-top: 2px;
margin-bottom: 2px;
}
&__address {
margin-left: 6px;
font-size: 14px;
}
}

@ -19,11 +19,14 @@ export default class ConfirmPageContainer extends Component {
subtitleComponent: PropTypes.node,
title: PropTypes.string,
titleComponent: PropTypes.node,
hideSenderToRecipient: PropTypes.bool,
showAccountInHeader: PropTypes.bool,
// Sender to Recipient
fromAddress: PropTypes.string,
fromName: PropTypes.string,
toAddress: PropTypes.string,
toName: PropTypes.string,
toEns: PropTypes.string,
toNickname: PropTypes.string,
// Content
contentComponent: PropTypes.node,
@ -69,6 +72,7 @@ export default class ConfirmPageContainer extends Component {
fromName,
fromAddress,
toName,
toEns,
toNickname,
toAddress,
disabled,
@ -102,6 +106,8 @@ export default class ConfirmPageContainer extends Component {
lastTx,
ofText,
requestsWaitingText,
hideSenderToRecipient,
showAccountInHeader,
} = this.props
const renderAssetImage = contentComponent || (!contentComponent && !identiconAddress)
@ -122,15 +128,21 @@ export default class ConfirmPageContainer extends Component {
<ConfirmPageContainerHeader
showEdit={showEdit}
onEdit={() => onEdit()}
showAccountInHeader={showAccountInHeader}
accountAddress={fromAddress}
>
<SenderToRecipient
senderName={fromName}
senderAddress={fromAddress}
recipientName={toName}
recipientAddress={toAddress}
recipientNickname={toNickname}
assetImage={renderAssetImage ? assetImage : undefined}
/>
{ hideSenderToRecipient
? null
: <SenderToRecipient
senderName={fromName}
senderAddress={fromAddress}
recipientName={toName}
recipientAddress={toAddress}
recipientEns={toEns}
recipientNickname={toNickname}
assetImage={renderAssetImage ? assetImage : undefined}
/>
}
</ConfirmPageContainerHeader>
{
contentComponent || (

@ -5,3 +5,9 @@
@import 'confirm-detail-row/index';
@import 'confirm-page-container-navigation/index';
.page-container {
&__content-component-wrapper {
height: 100%;
}
}

@ -11,8 +11,8 @@ export default class AdvancedGasInputs extends Component {
static propTypes = {
updateCustomGasPrice: PropTypes.func,
updateCustomGasLimit: PropTypes.func,
customGasPrice: PropTypes.number,
customGasLimit: PropTypes.number,
customGasPrice: PropTypes.number.isRequired,
customGasLimit: PropTypes.number.isRequired,
insufficientBalance: PropTypes.bool,
customPriceIsSafe: PropTypes.bool,
isSpeedUp: PropTypes.bool,

@ -12,7 +12,7 @@ function convertGasPriceForInputs (gasPriceInHexWEI) {
}
function convertGasLimitForInputs (gasLimitInHexWEI) {
return parseInt(gasLimitInHexWEI, 16)
return parseInt(gasLimitInHexWEI, 16) || 0
}
const mapDispatchToProps = dispatch => {

@ -1,5 +1,8 @@
import React, { Component } from 'react'
import PropTypes from 'prop-types'
import {
decGWEIToHexWEI,
} from '../../../../../helpers/utils/conversions.util'
import Loading from '../../../../ui/loading-screen'
import GasPriceChart from '../../gas-price-chart'
import AdvancedGasInputs from '../../advanced-gas-inputs'
@ -42,6 +45,11 @@ export default class AdvancedTabContent extends Component {
)
}
onGasChartUpdate = (price) => {
const { updateCustomGasPrice } = this.props
updateCustomGasPrice(decGWEIToHexWEI(price))
}
render () {
const { t } = this.context
const {
@ -78,7 +86,7 @@ export default class AdvancedTabContent extends Component {
? <div>
<div className="advanced-tab__fee-chart__title">{ t('liveGasPricePredictions') }</div>
{!gasEstimatesLoading
? <GasPriceChart {...gasChartProps} updateCustomGasPrice={updateCustomGasPrice} />
? <GasPriceChart {...gasChartProps} updateCustomGasPrice={this.onGasChartUpdate} />
: <Loading />
}
<div className="advanced-tab__fee-chart__speed-buttons">

@ -34,8 +34,6 @@ import {
preferencesSelector,
} from '../../../../selectors/selectors.js'
import {
formatTimeEstimate,
getFastPriceEstimateInHexWEI,
getBasicGasEstimateLoadingStatus,
getGasEstimatesLoadingStatus,
getCustomGasLimit,
@ -47,6 +45,9 @@ import {
getBasicGasEstimateBlockTime,
isCustomPriceSafe,
} from '../../../../selectors/custom-gas'
import {
getTxParams,
} from '../../../../selectors/transactions'
import {
getTokenBalance,
} from '../../../../pages/send/send.selectors'
@ -59,6 +60,7 @@ import {
decEthToConvertedCurrency as ethTotalToConvertedCurrency,
hexWEIToDecGWEI,
} from '../../../../helpers/utils/conversions.util'
import { getRenderableTimeEstimate } from '../../../../helpers/utils/gas-time-estimates.util'
import {
formatETHFee,
} from '../../../../helpers/utils/formatters'
@ -67,7 +69,6 @@ import {
isBalanceSufficient,
} from '../../../../pages/send/send.utils'
import { addHexPrefix } from 'ethereumjs-util'
import { getAdjacentGasPrices, extrapolateY } from '../gas-price-chart/gas-price-chart.utils'
import { getMaxModeOn } from '../../../../pages/send/send-content/send-amount-row/amount-max-button/amount-max-button.selectors'
import { calcMaxAmount } from '../../../../pages/send/send-content/send-amount-row/amount-max-button/amount-max-button.utils'
@ -83,7 +84,7 @@ const mapStateToProps = (state, ownProps) => {
const { gasPrice: currentGasPrice, gas: currentGasLimit, value } = getTxParams(state, selectedTransaction)
const customModalGasPriceInHex = getCustomGasPrice(state) || currentGasPrice
const customModalGasLimitInHex = getCustomGasLimit(state) || currentGasLimit
const customModalGasLimitInHex = getCustomGasLimit(state) || currentGasLimit || '0x5208'
const customGasTotal = calcGasTotal(customModalGasLimitInHex, customModalGasPriceInHex)
const gasButtonInfo = getRenderableBasicEstimateData(state, customModalGasLimitInHex)
@ -301,18 +302,6 @@ function calcCustomGasLimit (customGasLimitInHex) {
return parseInt(customGasLimitInHex, 16)
}
function getTxParams (state, selectedTransaction = {}) {
const { metamask: { send } } = state
const { txParams } = selectedTransaction
return txParams || {
from: send.from,
gas: send.gasLimit || '0x5208',
gasPrice: send.gasPrice || getFastPriceEstimateInHexWEI(state, true),
to: send.to,
value: getSelectedToken(state) ? '0x0' : send.amount,
}
}
function addHexWEIsToRenderableEth (aHexWEI, bHexWEI) {
return pipe(
addHexWEIsToDec,
@ -334,31 +323,3 @@ function addHexWEIsToRenderableFiat (aHexWEI, bHexWEI, convertedCurrency, conver
partialRight(formatCurrency, [convertedCurrency]),
)(aHexWEI, bHexWEI)
}
function getRenderableTimeEstimate (currentGasPrice, gasPrices, estimatedTimes) {
const minGasPrice = gasPrices[0]
const maxGasPrice = gasPrices[gasPrices.length - 1]
let priceForEstimation = currentGasPrice
if (currentGasPrice < minGasPrice) {
priceForEstimation = minGasPrice
} else if (currentGasPrice > maxGasPrice) {
priceForEstimation = maxGasPrice
}
const {
closestLowerValueIndex,
closestHigherValueIndex,
closestHigherValue,
closestLowerValue,
} = getAdjacentGasPrices({ gasPrices, priceToPosition: priceForEstimation })
const newTimeEstimate = extrapolateY({
higherY: estimatedTimes[closestHigherValueIndex],
lowerY: estimatedTimes[closestLowerValueIndex],
higherX: closestHigherValue,
lowerX: closestLowerValue,
xForExtrapolation: priceForEstimation,
})
return formatTimeEstimate(newTimeEstimate, currentGasPrice > maxGasPrice, currentGasPrice < minGasPrice)
}

@ -1,11 +1,12 @@
import * as d3 from 'd3'
import c3 from 'c3'
import BigNumber from 'bignumber.js'
const newBigSigDig = n => (new BigNumber(n.toPrecision(15)))
const createOp = (a, b, op) => (newBigSigDig(a))[op](newBigSigDig(b))
const bigNumMinus = (a = 0, b = 0) => createOp(a, b, 'minus')
const bigNumDiv = (a = 0, b = 1) => createOp(a, b, 'div')
import {
extrapolateY,
getAdjacentGasPrices,
newBigSigDig,
bigNumMinus,
bigNumDiv,
} from '../../../../helpers/utils/gas-time-estimates.util'
export function handleMouseMove ({ xMousePos, chartXStart, chartWidth, gasPrices, estimatedTimes, chart }) {
const { currentPosValue, newTimeEstimate } = getNewXandTimeEstimate({
@ -66,25 +67,6 @@ export function handleChartUpdate ({ chart, gasPrices, newPrice, cssId }) {
}
}
export function getAdjacentGasPrices ({ gasPrices, priceToPosition }) {
const closestLowerValueIndex = gasPrices.findIndex((e, i, a) => e <= priceToPosition && a[i + 1] >= priceToPosition)
const closestHigherValueIndex = gasPrices.findIndex((e) => e > priceToPosition)
return {
closestLowerValueIndex,
closestHigherValueIndex,
closestHigherValue: gasPrices[closestHigherValueIndex],
closestLowerValue: gasPrices[closestLowerValueIndex],
}
}
export function extrapolateY ({ higherY = 0, lowerY = 0, higherX = 0, lowerX = 0, xForExtrapolation = 0 }) {
const slope = bigNumMinus(higherY, lowerY).div(bigNumMinus(higherX, lowerX))
const newTimeEstimate = slope.times(bigNumMinus(higherX, xForExtrapolation)).minus(newBigSigDig(higherY)).negated()
return newTimeEstimate.toNumber()
}
export function getNewXandTimeEstimate ({ xMousePos, chartXStart, chartWidth, gasPrices, estimatedTimes }) {
const chartMouseXPos = bigNumMinus(xMousePos, chartXStart)
const posPercentile = bigNumDiv(chartMouseXPos, chartWidth)

@ -84,4 +84,4 @@
@import 'home-notification/index';
@import 'multiple-notifications/index';
@import 'signature-request/index';

@ -1,10 +1,13 @@
import React, { PureComponent } from 'react'
import PropTypes from 'prop-types'
import Button from '../../ui/button'
import classnames from 'classnames'
export default class Modal extends PureComponent {
static propTypes = {
children: PropTypes.node,
contentClass: PropTypes.string,
containerClass: PropTypes.string,
// Header text
headerText: PropTypes.string,
onClose: PropTypes.func,
@ -36,10 +39,12 @@ export default class Modal extends PureComponent {
onCancel,
cancelType,
cancelText,
contentClass,
containerClass,
} = this.props
return (
<div className="modal-container">
<div className={classnames('modal-container', containerClass)}>
{
headerText && (
<div className="modal-container__header">
@ -53,7 +58,7 @@ export default class Modal extends PureComponent {
</div>
)
}
<div className="modal-container__content">
<div className={classnames('modal-container__content', contentClass)}>
{ children }
</div>
<div className="modal-container__footer">

@ -0,0 +1,170 @@
import React, { PureComponent } from 'react'
import PropTypes from 'prop-types'
import Modal from '../../modal'
import Identicon from '../../../ui/identicon'
import TextField from '../../../ui/text-field'
import classnames from 'classnames'
export default class EditApprovalPermission extends PureComponent {
static propTypes = {
hideModal: PropTypes.func.isRequired,
selectedIdentity: PropTypes.object,
tokenAmount: PropTypes.string,
customTokenAmount: PropTypes.string,
tokenSymbol: PropTypes.string,
tokenBalance: PropTypes.string,
setCustomAmount: PropTypes.func,
origin: PropTypes.string,
}
static contextTypes = {
t: PropTypes.func,
}
state = {
customSpendLimit: this.props.customTokenAmount,
selectedOptionIsUnlimited: !this.props.customTokenAmount,
}
renderModalContent () {
const { t } = this.context
const {
hideModal,
selectedIdentity,
tokenAmount,
tokenSymbol,
tokenBalance,
customTokenAmount,
origin,
} = this.props
const { name, address } = selectedIdentity || {}
const { selectedOptionIsUnlimited } = this.state
return (
<div className="edit-approval-permission">
<div className="edit-approval-permission__header">
<div className="edit-approval-permission__title">
{ t('editPermission') }
</div>
<div
className="edit-approval-permission__header__close"
onClick={() => hideModal()}
/>
</div>
<div className="edit-approval-permission__account-info">
<div className="edit-approval-permission__account-info__account">
<Identicon
address={address}
diameter={32}
/>
<div className="edit-approval-permission__account-info__name">{ name }</div>
<div>{ t('balance') }</div>
</div>
<div className="edit-approval-permission__account-info__balance">
{`${tokenBalance} ${tokenSymbol}`}
</div>
</div>
<div className="edit-approval-permission__edit-section">
<div className="edit-approval-permission__edit-section__title">
{ t('spendLimitPermission') }
</div>
<div className="edit-approval-permission__edit-section__description">
{ t('allowWithdrawAndSpend', [origin]) }
</div>
<div className="edit-approval-permission__edit-section__option">
<div
className="edit-approval-permission__edit-section__radio-button"
onClick={() => this.setState({ selectedOptionIsUnlimited: true })}
>
<div className={classnames({
'edit-approval-permission__edit-section__radio-button-outline': !selectedOptionIsUnlimited,
'edit-approval-permission__edit-section__radio-button-outline--selected': selectedOptionIsUnlimited,
})} />
<div className="edit-approval-permission__edit-section__radio-button-fill" />
{ selectedOptionIsUnlimited && <div className="edit-approval-permission__edit-section__radio-button-dot" />}
</div>
<div className="edit-approval-permission__edit-section__option-text">
<div className={classnames({
'edit-approval-permission__edit-section__option-label': !selectedOptionIsUnlimited,
'edit-approval-permission__edit-section__option-label--selected': selectedOptionIsUnlimited,
})}>
{
tokenAmount < tokenBalance
? t('proposedApprovalLimit')
: t('unlimited')
}
</div>
<div className="edit-approval-permission__edit-section__option-description" >
{ t('spendLimitRequestedBy', [origin]) }
</div>
<div className="edit-approval-permission__edit-section__option-value" >
{`${tokenAmount} ${tokenSymbol}`}
</div>
</div>
</div>
<div className="edit-approval-permission__edit-section__option">
<div
className="edit-approval-permission__edit-section__radio-button"
onClick={() => this.setState({ selectedOptionIsUnlimited: false })}
>
<div className={classnames({
'edit-approval-permission__edit-section__radio-button-outline': selectedOptionIsUnlimited,
'edit-approval-permission__edit-section__radio-button-outline--selected': !selectedOptionIsUnlimited,
})} />
<div className="edit-approval-permission__edit-section__radio-button-fill" />
{ !selectedOptionIsUnlimited && <div className="edit-approval-permission__edit-section__radio-button-dot" />}
</div>
<div className="edit-approval-permission__edit-section__option-text">
<div className={classnames({
'edit-approval-permission__edit-section__option-label': selectedOptionIsUnlimited,
'edit-approval-permission__edit-section__option-label--selected': !selectedOptionIsUnlimited,
})}>
{ t('customSpendLimit') }
</div>
<div className="edit-approval-permission__edit-section__option-description" >
{ t('enterMaxSpendLimit') }
</div>
<div className="edit-approval-permission__edit-section__option-input" >
<TextField
type="number"
min="0"
placeholder={ `${customTokenAmount || tokenAmount} ${tokenSymbol}` }
onChange={(event) => {
this.setState({ customSpendLimit: event.target.value })
if (selectedOptionIsUnlimited) {
this.setState({ selectedOptionIsUnlimited: false })
}
}}
fullWidth
margin="dense"
value={ this.state.customSpendLimit }
/>
</div>
</div>
</div>
</div>
</div>
)
}
render () {
const { t } = this.context
const { setCustomAmount, hideModal, customTokenAmount } = this.props
const { selectedOptionIsUnlimited, customSpendLimit } = this.state
return (
<Modal
onSubmit={() => {
setCustomAmount(!selectedOptionIsUnlimited ? customSpendLimit : '')
hideModal()
}}
submitText={t('save')}
submitType="primary"
contentClass="edit-approval-permission-modal-content"
containerClass="edit-approval-permission-modal-container"
submitDisabled={ (customSpendLimit === customTokenAmount) && !selectedOptionIsUnlimited }
>
{ this.renderModalContent() }
</Modal>
)
}
}

@ -0,0 +1,18 @@
import { connect } from 'react-redux'
import { compose } from 'recompose'
import withModalProps from '../../../../helpers/higher-order-components/with-modal-props'
import EditApprovalPermission from './edit-approval-permission.component'
import { getSelectedIdentity } from '../../../../selectors/selectors'
const mapStateToProps = (state) => {
const modalStateProps = state.appState.modal.modalState.props || {}
return {
selectedIdentity: getSelectedIdentity(state),
...modalStateProps,
}
}
export default compose(
withModalProps,
connect(mapStateToProps)
)(EditApprovalPermission)

@ -0,0 +1 @@
export { default } from './edit-approval-permission.container'

@ -0,0 +1,167 @@
.edit-approval-permission {
width: 100%;
&__header,
&__account-info {
display: flex;
justify-content: center;
align-items: center;
position: relative;
border-bottom: 1px solid #d2d8dd;
}
&__header {
padding: 24px;
&__close {
position: absolute;
right: 24px;
background-image: url("/images/close-gray.svg");
width: .75rem;
height: .75rem;
cursor: pointer;
}
}
&__title {
font-weight: bold;
font-size: 18px;
line-height: 25px;
}
&__account-info {
justify-content: space-between;
padding: 8px 24px;
&__account,
&__balance {
font-weight: normal;
font-size: 14px;
color: #24292E;
}
&__account {
display: flex;
align-items: center;
}
&__name {
margin-left: 8px;
margin-right: 8px;
}
&__balance {
color: #6A737D;
}
}
&__edit-section {
padding: 24px;
&__title {
font-weight: bold;
font-size: 14px;
line-height: 20px;
color: #24292E;
}
&__description {
font-weight: normal;
font-size: 12px;
line-height: 17px;
color: #6A737D;
margin-top: 8px;
}
&__option {
display: flex;
align-items: flex-start;
margin-top: 20px;
}
&__radio-button {
width: 18px;
}
&__option-text {
display: flex;
flex-direction: column;
}
&__option-label,
&__option-label--selected {
font-weight: normal;
font-size: 14px;
line-height: 20px;
color: #474B4D;
}
&__option-label--selected {
color: #037DD6;
}
&__option-description {
font-weight: normal;
font-size: 12px;
line-height: 17px;
color: #6A737D;
margin-top: 8px;
margin-bottom: 6px;
}
&__option-value {
font-weight: normal;
font-size: 18px;
line-height: 25px;
color: #24292E;
}
&__radio-button {
position: relative;
width: 18px;
height: 18px;
display: flex;
justify-content: center;
align-items: center;
margin-right: 4px;
}
&__radio-button-outline,
&__radio-button-outline--selected {
width: 18px;
height: 18px;
background: #DADCDD;
border-radius: 9px;
position: absolute;
}
&__radio-button-outline--selected {
background: #037DD6;
}
&__radio-button-fill {
width: 14px;
height: 14px;
background: white;
border-radius: 7px;
position: absolute;
}
&__radio-button-dot {
width: 8px;
height: 8px;
background: #037DD6;
border-radius: 4px;
position: absolute;
}
}
}
.edit-approval-permission-modal-content {
padding: 0px;
}
.edit-approval-permission-modal-container {
max-height: 550px;
width: 100%;
}

@ -9,3 +9,5 @@
@import 'metametrics-opt-in-modal/index';
@import './add-to-addressbook-modal/index';
@import './edit-approval-permission/index';

@ -28,6 +28,7 @@ import ClearApprovedOrigins from './clear-approved-origins'
import ConfirmCustomizeGasModal from '../gas-customization/gas-modal-page-container'
import ConfirmDeleteNetwork from './confirm-delete-network'
import AddToAddressBookModal from './add-to-addressbook-modal'
import EditApprovalPermission from './edit-approval-permission'
const modalContainerBaseStyle = {
transform: 'translate3d(-50%, 0, 0px)',
@ -304,6 +305,31 @@ const MODALS = {
},
},
EDIT_APPROVAL_PERMISSION: {
contents: h(EditApprovalPermission),
mobileModalStyle: {
width: '95vw',
height: '100vh',
top: '50px',
transform: 'none',
left: '0',
right: '0',
margin: '0 auto',
},
laptopModalStyle: {
width: 'auto',
height: '0px',
top: '80px',
left: '0px',
transform: 'none',
margin: '0 auto',
position: 'relative',
},
contentStyle: {
borderRadius: '8px',
},
},
TRANSACTION_CONFIRMED: {
disableBackdropClick: true,
contents: h(TransactionConfirmed),

@ -4,7 +4,7 @@ import PropTypes from 'prop-types'
export default class MultipleNotifications extends PureComponent {
static propTypes = {
notifications: PropTypes.array,
children: PropTypes.array,
classNames: PropTypes.array,
}
@ -14,11 +14,10 @@ export default class MultipleNotifications extends PureComponent {
render () {
const { showAll } = this.state
const { notifications, classNames = [] } = this.props
const { children, classNames = [] } = this.props
const notificationsToBeRendered = notifications.filter(notificationConfig => notificationConfig.shouldBeRendered)
if (notificationsToBeRendered.length === 0) {
const childrenToRender = children.filter(child => child)
if (childrenToRender.length === 0) {
return null
}
@ -29,12 +28,12 @@ export default class MultipleNotifications extends PureComponent {
'home-notification-wrapper--show-first': !showAll,
})}
>
{ notificationsToBeRendered.map(notificationConfig => notificationConfig.component) }
{ childrenToRender }
<div
className="home-notification-wrapper__i-container"
onClick={() => this.setState({ showAll: !showAll })}
>
{notificationsToBeRendered.length > 1 ? <i className={classnames('fa fa-sm fa-sort-amount-asc', {
{childrenToRender.length > 1 ? <i className={classnames('fa fa-sm fa-sort-amount-asc', {
'flipped': !showAll,
})} /> : null}
</div>

@ -2,6 +2,8 @@ import PropTypes from 'prop-types'
import React, {PureComponent} from 'react'
import { ProviderPageContainerContent, ProviderPageContainerHeader } from '.'
import { PageContainerFooter } from '../../ui/page-container'
import { ENVIRONMENT_TYPE_NOTIFICATION } from '../../../../../app/scripts/lib/enums'
import { getEnvironmentType } from '../../../../../app/scripts/lib/util'
export default class ProviderPageContainer extends PureComponent {
static propTypes = {
@ -20,6 +22,9 @@ export default class ProviderPageContainer extends PureComponent {
};
componentDidMount () {
if (getEnvironmentType(window.location.href) === ENVIRONMENT_TYPE_NOTIFICATION) {
window.addEventListener('beforeunload', this._beforeUnload)
}
this.context.metricsEvent({
eventOpts: {
category: 'Auth',
@ -29,6 +34,27 @@ export default class ProviderPageContainer extends PureComponent {
})
}
_beforeUnload () {
const { origin, rejectProviderRequestByOrigin } = this.props
this.context.metricsEvent({
eventOpts: {
category: 'Auth',
action: 'Connect',
name: 'Cancel Connect Request Via Notification Close',
},
})
this._removeBeforeUnload()
rejectProviderRequestByOrigin(origin)
}
_removeBeforeUnload () {
window.removeEventListener('beforeunload', this._beforeUnload)
}
componentWillUnmount () {
this._removeBeforeUnload()
}
onCancel = () => {
const { origin, rejectProviderRequestByOrigin } = this.props
this.context.metricsEvent({
@ -38,6 +64,7 @@ export default class ProviderPageContainer extends PureComponent {
name: 'Canceled',
},
})
this._removeBeforeUnload()
rejectProviderRequestByOrigin(origin)
}
@ -50,6 +77,7 @@ export default class ProviderPageContainer extends PureComponent {
name: 'Confirmed',
},
})
this._removeBeforeUnload()
approveProviderRequestByOrigin(origin)
}

@ -103,24 +103,36 @@ function SignatureRequest (props) {
}
}
SignatureRequest.prototype.componentDidMount = function () {
SignatureRequest.prototype._beforeUnload = (event) => {
const { clearConfirmTransaction, cancel } = this.props
const { metricsEvent } = this.context
metricsEvent({
eventOpts: {
category: 'Transactions',
action: 'Sign Request',
name: 'Cancel Sig Request Via Notification Close',
},
})
clearConfirmTransaction()
cancel(event)
}
SignatureRequest.prototype._removeBeforeUnload = () => {
if (getEnvironmentType(window.location.href) === ENVIRONMENT_TYPE_NOTIFICATION) {
window.onbeforeunload = event => {
metricsEvent({
eventOpts: {
category: 'Transactions',
action: 'Sign Request',
name: 'Cancel Sig Request Via Notification Close',
},
})
clearConfirmTransaction()
cancel(event)
}
window.removeEventListener('beforeunload', this._beforeUnload)
}
}
SignatureRequest.prototype.componentDidMount = function () {
if (getEnvironmentType(window.location.href) === ENVIRONMENT_TYPE_NOTIFICATION) {
window.addEventListener('beforeunload', this._beforeUnload)
}
}
SignatureRequest.prototype.componentWillUnmount = function () {
this._removeBeforeUnload()
}
SignatureRequest.prototype.renderHeader = function () {
return h('div.request-signature__header', [
@ -236,7 +248,7 @@ SignatureRequest.prototype.renderBody = function () {
let notice = this.context.t('youSign') + ':'
const { txData } = this.props
const { type, msgParams: { data, version } } = txData
const { type, msgParams: { data } } = txData
if (type === 'personal_sign') {
rows = [{ name: this.context.t('message'), value: this.msgHexToText(data) }]
@ -268,17 +280,15 @@ SignatureRequest.prototype.renderBody = function () {
}, [notice]),
h('div.request-signature__rows',
type === 'eth_signTypedData' && (version === 'V3' || version === 'V4') ?
this.renderTypedData(data) :
rows.map(({ name, value }) => {
if (typeof value === 'boolean') {
value = value.toString()
}
return h('div.request-signature__row', [
h('div.request-signature__row-title', [`${name}:`]),
h('div.request-signature__row-value', value),
])
}),
rows.map(({ name, value }, index) => {
if (typeof value === 'boolean') {
value = value.toString()
}
return h('div.request-signature__row', { key: `request-signature-row-${index}` }, [
h('div.request-signature__row-title', [`${name}:`]),
h('div.request-signature__row-value', value),
])
})
),
])
}
@ -292,6 +302,7 @@ SignatureRequest.prototype.renderFooter = function () {
large: true,
className: 'request-signature__footer__cancel-button',
onClick: event => {
this._removeBeforeUnload()
cancel(event).then(() => {
this.context.metricsEvent({
eventOpts: {
@ -310,6 +321,7 @@ SignatureRequest.prototype.renderFooter = function () {
large: true,
className: 'request-signature__footer__sign-button',
onClick: event => {
this._removeBeforeUnload()
sign(event).then(() => {
this.context.metricsEvent({
eventOpts: {

@ -0,0 +1 @@
export { default } from './signature-request.container'

@ -0,0 +1,96 @@
@import 'signature-request-footer/index';
@import 'signature-request-header/index';
@import 'signature-request-message/index';
.signature-request {
display: flex;
flex: 1 1 auto;
flex-direction: column;
min-width: 0;
@media screen and (min-width: 576px) {
flex: initial;
}
}
.signature-request-header {
flex: 1;
.network-display__container {
padding: 0;
justify-content: flex-end;
}
.network-display__name {
font-size: 12px;
white-space: nowrap;
font-weight: 500;
}
}
.signature-request-content {
flex: 1 40%;
margin-top: 1rem;
display: flex;
align-items: center;
flex-direction: column;
margin-bottom: 25px;
min-height: min-content;
&__title {
font-family: Roboto;
font-style: normal;
font-weight: 500;
font-size: 18px;
}
&__identicon-container {
padding: 1rem;
flex: 1;
position: relative;
width: 100%;
display: flex;
justify-content: center;
align-items: center;
}
&__identicon-border {
height: 75px;
width: 75px;
border-radius: 50%;
border: 1px solid white;
position: absolute;
box-shadow: 0 2px 2px 0.5px rgba(0, 0, 0, 0.19);
}
&__identicon-initial {
position: absolute;
font-family: Roboto;
font-style: normal;
font-weight: 500;
font-size: 60px;
color: white;
z-index: 1;
text-shadow: 0px 4px 6px rgba(0, 0, 0, 0.422);
}
&__info {
font-size: 12px;
}
&__info--bolded {
font-size: 16px;
font-weight: 500;
}
p {
color: #999999;
font-size: 0.8rem;
}
.identicon {}
}
.signature-request-footer {
flex: 1 1 auto;
}

@ -0,0 +1 @@
export { default } from './signature-request-footer.component'

@ -0,0 +1,18 @@
.signature-request-footer {
display: flex;
border-top: 1px solid #d2d8dd;
button {
text-transform: uppercase;
flex: 1;
margin: 1rem 0.5rem;
border-radius: 3px;
}
button:first-child() {
margin-left: 1rem;
}
button:last-child() {
margin-right: 1rem;
}
}

@ -0,0 +1,24 @@
import React, { PureComponent } from 'react'
import PropTypes from 'prop-types'
import Button from '../../../ui/button'
export default class SignatureRequestFooter extends PureComponent {
static propTypes = {
cancelAction: PropTypes.func.isRequired,
signAction: PropTypes.func.isRequired,
}
static contextTypes = {
t: PropTypes.func,
}
render () {
const { cancelAction, signAction } = this.props
return (
<div className="signature-request-footer">
<Button onClick={cancelAction} type="default" large>{this.context.t('cancel')}</Button>
<Button onClick={signAction} type="primary" large>{this.context.t('sign')}</Button>
</div>
)
}
}

@ -0,0 +1 @@
export { default } from './signature-request-header.component'

@ -0,0 +1,25 @@
.signature-request-header {
display: flex;
padding: 1rem;
border-bottom: 1px solid $geyser;
justify-content: space-between;
font-size: .75rem;
&--account, &--network {
flex: 1;
}
&--account {
display: flex;
align-items: center;
.account-list-item__account-name {
font-size: 12px;
font-weight: 500;
}
.account-list-item__top-row {
margin: 0px;
}
}
}

@ -0,0 +1,29 @@
import React, { PureComponent } from 'react'
import PropTypes from 'prop-types'
import AccountListItem from '../../../../pages/send/account-list-item/account-list-item.component'
import NetworkDisplay from '../../network-display'
export default class SignatureRequestHeader extends PureComponent {
static propTypes = {
selectedAccount: PropTypes.object.isRequired,
}
render () {
const { selectedAccount } = this.props
return (
<div className="signature-request-header">
<div className="signature-request-header--account">
{selectedAccount && <AccountListItem
displayBalance={false}
account={selectedAccount}
/>}
{name}
</div>
<div className="signature-request-header--network">
<NetworkDisplay colored={false} />
</div>
</div>
)
}
}

@ -0,0 +1 @@
export { default } from './signature-request-message.component'

@ -0,0 +1,67 @@
.signature-request-message {
flex: 1 60%;
display: flex;
flex-direction: column;
&__title {
font-weight: 500;
font-size: 14px;
color: #636778;
margin-left: 12px;
}
h2 {
flex: 1 1 0;
text-align: left;
font-size: 0.8rem;
border-bottom: 1px solid #d2d8dd;
padding: 0.5rem;
margin: 0;
color: #ccc;
}
&--root {
flex: 1 100%;
background-color: #f8f9fb;
padding-bottom: 0.5rem;
overflow: auto;
padding-left: 12px;
padding-right: 12px;
width: 360px;
font-family: monospace;
@media screen and (min-width: 576px) {
width: auto;
}
}
&__type-title {
font-family: monospace;
font-style: normal;
font-weight: normal;
font-size: 14px;
margin-left: 12px;
margin-top: 6px;
margin-bottom: 10px;
}
&--node, &--node-leaf {
padding-left: 0.8rem;
&-label {
color: #5B5D67;
}
&-value {
color: black;
margin-left: 0.5rem;
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
}
}
&--node-leaf {
display: flex;
}
}

@ -0,0 +1,50 @@
import React, { PureComponent } from 'react'
import PropTypes from 'prop-types'
import classnames from 'classnames'
export default class SignatureRequestMessage extends PureComponent {
static propTypes = {
data: PropTypes.object.isRequired,
}
static contextTypes = {
t: PropTypes.func,
}
renderNode (data) {
return (
<div className="signature-request-message--node">
{Object.entries(data).map(([ label, value ], i) => (
<div
className={classnames('signature-request-message--node', {
'signature-request-message--node-leaf': typeof value !== 'object' || value === null,
})}
key={i}
>
<span className="signature-request-message--node-label">{label}: </span>
{
typeof value === 'object' && value !== null ?
this.renderNode(value)
: <span className="signature-request-message--node-value">{value}</span>
}
</div>
))}
</div>
)
}
render () {
const { data } = this.props
return (
<div className="signature-request-message">
<div className="signature-request-message__title">{this.context.t('signatureRequest1')}</div>
<div className="signature-request-message--root">
<div className="signature-request-message__type-title">{this.context.t('signatureRequest1')}</div>
{this.renderNode(data)}
</div>
</div>
)
}
}

@ -0,0 +1,81 @@
import React, { PureComponent } from 'react'
import PropTypes from 'prop-types'
import Header from './signature-request-header'
import Footer from './signature-request-footer'
import Message from './signature-request-message'
import { ENVIRONMENT_TYPE_NOTIFICATION } from './signature-request.constants'
import { getEnvironmentType } from '../../../../../app/scripts/lib/util'
import Identicon from '../../ui/identicon'
export default class SignatureRequest extends PureComponent {
static propTypes = {
txData: PropTypes.object.isRequired,
selectedAccount: PropTypes.shape({
address: PropTypes.string,
balance: PropTypes.string,
name: PropTypes.string,
}).isRequired,
clearConfirmTransaction: PropTypes.func.isRequired,
cancel: PropTypes.func.isRequired,
sign: PropTypes.func.isRequired,
}
static contextTypes = {
t: PropTypes.func,
}
componentDidMount () {
const { clearConfirmTransaction, cancel } = this.props
const { metricsEvent } = this.context
if (getEnvironmentType(window.location.href) === ENVIRONMENT_TYPE_NOTIFICATION) {
window.addEventListener('beforeunload', (event) => {
metricsEvent({
eventOpts: {
category: 'Transactions',
action: 'Sign Request',
name: 'Cancel Sig Request Via Notification Close',
},
})
clearConfirmTransaction()
cancel(event)
})
}
}
formatWallet (wallet) {
return `${wallet.slice(0, 8)}...${wallet.slice(wallet.length - 8, wallet.length)}`
}
render () {
const {
selectedAccount,
txData: { msgParams: { data, origin, from: senderWallet }},
cancel,
sign,
} = this.props
const { message } = JSON.parse(data)
return (
<div className="signature-request page-container">
<Header selectedAccount={selectedAccount} />
<div className="signature-request-content">
<div className="signature-request-content__title">{this.context.t('sigRequest')}</div>
<div className="signature-request-content__identicon-container">
<div className="signature-request-content__identicon-initial" >{ message.from.name && message.from.name[0] }</div>
<div className="signature-request-content__identicon-border" />
<Identicon
address={message.from.wallet}
diameter={70}
/>
</div>
<div className="signature-request-content__info--bolded">{message.from.name}</div>
<div className="signature-request-content__info">{origin}</div>
<div className="signature-request-content__info">{this.formatWallet(senderWallet)}</div>
</div>
<Message data={message} />
<Footer cancelAction={cancel} signAction={sign} />
</div>
)
}
}

@ -0,0 +1,3 @@
import { ENVIRONMENT_TYPE_NOTIFICATION } from '../../../../../app/scripts/lib/enums'
export { ENVIRONMENT_TYPE_NOTIFICATION }

@ -0,0 +1,72 @@
import { connect } from 'react-redux'
import { withRouter } from 'react-router-dom'
import { compose } from 'recompose'
import SignatureRequest from './signature-request.component'
import { goHome } from '../../../store/actions'
import { clearConfirmTransaction } from '../../../ducks/confirm-transaction/confirm-transaction.duck'
import {
getSelectedAccount,
getCurrentAccountWithSendEtherInfo,
getSelectedAddress,
accountsWithSendEtherInfoSelector,
conversionRateSelector,
} from '../../../selectors/selectors.js'
function mapStateToProps (state) {
return {
balance: getSelectedAccount(state).balance,
selectedAccount: getCurrentAccountWithSendEtherInfo(state),
selectedAddress: getSelectedAddress(state),
accounts: accountsWithSendEtherInfoSelector(state),
conversionRate: conversionRateSelector(state),
}
}
function mapDispatchToProps (dispatch) {
return {
goHome: () => dispatch(goHome()),
clearConfirmTransaction: () => dispatch(clearConfirmTransaction()),
}
}
function mergeProps (stateProps, dispatchProps, ownProps) {
const {
signPersonalMessage,
signTypedMessage,
cancelPersonalMessage,
cancelTypedMessage,
signMessage,
cancelMessage,
txData,
} = ownProps
const { type } = txData
let cancel
let sign
if (type === 'personal_sign') {
cancel = cancelPersonalMessage
sign = signPersonalMessage
} else if (type === 'eth_signTypedData') {
cancel = cancelTypedMessage
sign = signTypedMessage
} else if (type === 'eth_sign') {
cancel = cancelMessage
sign = signMessage
}
return {
...stateProps,
...dispatchProps,
...ownProps,
txData,
cancel,
sign,
}
}
export default compose(
withRouter,
connect(mapStateToProps, mapDispatchToProps, mergeProps)
)(SignatureRequest)

@ -0,0 +1,25 @@
import React from 'react'
import assert from 'assert'
import shallow from '../../../../../lib/shallow-with-context'
import SignatureRequest from '../signature-request.component'
describe('Signature Request Component', function () {
let wrapper
beforeEach(() => {
wrapper = shallow(<SignatureRequest txData={{
msgParams: {
data: '{"message": {"from": {"name": "hello"}}}',
from: '0x123456789abcdef',
} }} />)
})
describe('render', () => {
it('should render a div with one child', () => {
assert(wrapper.is('div'))
assert.equal(wrapper.length, 1)
assert(wrapper.hasClass('signature-request'))
})
})
})

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

@ -17,6 +17,10 @@ export default class TransactionListItemDetails extends PureComponent {
metricsEvent: PropTypes.func,
}
static defaultProps = {
recipientEns: null,
}
static propTypes = {
onCancel: PropTypes.func,
onRetry: PropTypes.func,
@ -26,7 +30,11 @@ export default class TransactionListItemDetails extends PureComponent {
isEarliestNonce: PropTypes.bool,
cancelDisabled: PropTypes.bool,
transactionGroup: PropTypes.object,
recipientEns: PropTypes.string,
recipientAddress: PropTypes.string.isRequired,
rpcPrefs: PropTypes.object,
senderAddress: PropTypes.string.isRequired,
tryReverseResolveAddress: PropTypes.func.isRequired,
}
state = {
@ -82,6 +90,12 @@ export default class TransactionListItemDetails extends PureComponent {
})
}
async componentDidMount () {
const { recipientAddress, tryReverseResolveAddress } = this.props
tryReverseResolveAddress(recipientAddress)
}
renderCancel () {
const { t } = this.context
const {
@ -128,11 +142,14 @@ export default class TransactionListItemDetails extends PureComponent {
showRetry,
onCancel,
onRetry,
recipientEns,
recipientAddress,
rpcPrefs: { blockExplorerUrl } = {},
senderAddress,
isEarliestNonce,
} = this.props
const { primaryTransaction: transaction } = transactionGroup
const { hash, txParams: { to, from } = {} } = transaction
const { hash } = transaction
return (
<div className="transaction-list-item-details">
@ -192,8 +209,9 @@ export default class TransactionListItemDetails extends PureComponent {
<SenderToRecipient
variant={FLAT_VARIANT}
addressOnly
recipientAddress={to}
senderAddress={from}
recipientEns={recipientEns}
recipientAddress={recipientAddress}
senderAddress={senderAddress}
onRecipientClick={() => {
this.context.metricsEvent({
eventOpts: {

@ -0,0 +1,28 @@
import { connect } from 'react-redux'
import TransactionListItemDetails from './transaction-list-item-details.component'
import { checksumAddress } from '../../../helpers/utils/util'
import { tryReverseResolveAddress } from '../../../store/actions'
const mapStateToProps = (state, ownProps) => {
const { metamask } = state
const {
ensResolutionsByAddress,
} = metamask
const { recipientAddress } = ownProps
const address = checksumAddress(recipientAddress)
const recipientEns = ensResolutionsByAddress[address] || ''
return {
recipientEns,
}
}
const mapDispatchToProps = (dispatch) => {
return {
tryReverseResolveAddress: (address) => {
return dispatch(tryReverseResolveAddress(address))
},
}
}
export default connect(mapStateToProps, mapDispatchToProps)(TransactionListItemDetails)

@ -13,19 +13,19 @@
width: 100%;
padding: 16px 20px;
display: grid;
grid-template-columns: 45px 1fr 1fr 1fr;
grid-template-columns: 45px 1fr 1fr 1fr 1fr;
grid-template-areas:
"identicon action status primary-amount"
"identicon nonce status secondary-amount";
"identicon action status estimated-time primary-amount"
"identicon nonce status estimated-time secondary-amount";
grid-template-rows: 24px;
@media screen and (max-width: $break-small) {
padding: .5rem 1rem;
grid-template-columns: 45px 5fr 3fr;
grid-template-areas:
"nonce nonce nonce"
"identicon action primary-amount"
"identicon status secondary-amount";
"nonce nonce nonce nonce"
"identicon action estimated-time primary-amount"
"identicon status estimated-time secondary-amount";
grid-template-rows: auto 24px;
}
@ -65,6 +65,18 @@
}
}
&__estimated-time {
grid-area: estimated-time;
grid-row: 1 / span 2;
align-self: center;
@media screen and (max-width: $break-small) {
grid-row: 3;
grid-column: 4;
font-size: small;
}
}
&__nonce {
font-size: .75rem;
color: #5e6064;

@ -7,10 +7,13 @@ import TransactionAction from '../transaction-action'
import UserPreferencedCurrencyDisplay from '../user-preferenced-currency-display'
import TokenCurrencyDisplay from '../../ui/token-currency-display'
import TransactionListItemDetails from '../transaction-list-item-details'
import TransactionTimeRemaining from '../transaction-time-remaining'
import { CONFIRM_TRANSACTION_ROUTE } from '../../../helpers/constants/routes'
import { UNAPPROVED_STATUS, TOKEN_METHOD_TRANSFER } from '../../../helpers/constants/transactions'
import { PRIMARY, SECONDARY } from '../../../helpers/constants/common'
import { getStatusKey } from '../../../helpers/utils/transactions.util'
import { ENVIRONMENT_TYPE_FULLSCREEN } from '../../../../../app/scripts/lib/enums'
import { getEnvironmentType } from '../../../../../app/scripts/lib/util'
export default class TransactionListItem extends PureComponent {
static propTypes = {
@ -38,6 +41,8 @@ export default class TransactionListItem extends PureComponent {
data: PropTypes.string,
getContractMethodData: PropTypes.func,
isDeposit: PropTypes.bool,
transactionTimeFeatureActive: PropTypes.bool,
firstPendingTransactionId: PropTypes.number,
}
static defaultProps = {
@ -52,6 +57,13 @@ export default class TransactionListItem extends PureComponent {
showTransactionDetails: false,
}
componentDidMount () {
if (this.props.data) {
this.props.getContractMethodData(this.props.data)
}
}
handleClick = () => {
const {
transaction,
@ -162,12 +174,6 @@ export default class TransactionListItem extends PureComponent {
)
}
componentDidMount () {
if (this.props.data) {
this.props.getContractMethodData(this.props.data)
}
}
render () {
const {
assetImages,
@ -182,13 +188,21 @@ export default class TransactionListItem extends PureComponent {
transactionGroup,
rpcPrefs,
isEarliestNonce,
firstPendingTransactionId,
transactionTimeFeatureActive,
} = this.props
const { txParams = {} } = transaction
const { showTransactionDetails } = this.state
const fromAddress = txParams.from
const toAddress = tokenData
? tokenData.params && tokenData.params[0] && tokenData.params[0].value || txParams.to
: txParams.to
const isFullScreen = getEnvironmentType(window.location.href) === ENVIRONMENT_TYPE_FULLSCREEN
const showEstimatedTime = transactionTimeFeatureActive &&
(transaction.id === firstPendingTransactionId) &&
isFullScreen
return (
<div className="transaction-list-item">
<div
@ -221,6 +235,13 @@ export default class TransactionListItem extends PureComponent {
: primaryTransaction.err && primaryTransaction.err.message
)}
/>
{ showEstimatedTime
? <TransactionTimeRemaining
className="transaction-list-item__estimated-time"
transaction={ primaryTransaction }
/>
: null
}
{ this.renderPrimaryCurrency() }
{ this.renderSecondaryCurrency() }
</div>
@ -240,6 +261,8 @@ export default class TransactionListItem extends PureComponent {
showCancel={showCancel}
cancelDisabled={!hasEnoughCancelGas}
rpcPrefs={rpcPrefs}
senderAddress={fromAddress}
recipientAddress={toAddress}
/>
</div>
)

@ -8,12 +8,19 @@ import { getTokenData } from '../../../helpers/utils/transactions.util'
import { getHexGasTotal, increaseLastGasPrice } from '../../../helpers/utils/confirm-tx.util'
import { formatDate } from '../../../helpers/utils/util'
import {
fetchBasicGasAndTimeEstimates,
fetchGasEstimates,
fetchBasicGasAndTimeEstimates,
setCustomGasPriceForRetry,
setCustomGasLimit,
} from '../../../ducks/gas/gas.duck'
import { getIsMainnet, preferencesSelector, getSelectedAddress, conversionRateSelector, getKnownMethodData } from '../../../selectors/selectors'
import {
getIsMainnet,
preferencesSelector,
getSelectedAddress,
conversionRateSelector,
getKnownMethodData,
getFeatureFlags,
} from '../../../selectors/selectors'
import { isBalanceSufficient } from '../../../pages/send/send.utils'
const mapStateToProps = (state, ownProps) => {
@ -38,6 +45,8 @@ const mapStateToProps = (state, ownProps) => {
conversionRate: conversionRateSelector(state),
})
const transactionTimeFeatureActive = getFeatureFlags(state).transactionTime
return {
methodData: getKnownMethodData(state, data) || {},
showFiat: (isMainnet || !!showFiatInTestnets),
@ -45,6 +54,7 @@ const mapStateToProps = (state, ownProps) => {
hasEnoughCancelGas,
rpcPrefs,
isDeposit,
transactionTimeFeatureActive,
}
}

@ -22,19 +22,50 @@ export default class TransactionList extends PureComponent {
selectedToken: PropTypes.object,
updateNetworkNonce: PropTypes.func,
assetImages: PropTypes.object,
fetchBasicGasAndTimeEstimates: PropTypes.func,
fetchGasEstimates: PropTypes.func,
transactionTimeFeatureActive: PropTypes.bool,
firstPendingTransactionId: PropTypes.number,
}
componentDidMount () {
this.props.updateNetworkNonce()
const {
pendingTransactions,
updateNetworkNonce,
fetchBasicGasAndTimeEstimates,
fetchGasEstimates,
transactionTimeFeatureActive,
} = this.props
updateNetworkNonce()
if (transactionTimeFeatureActive && pendingTransactions.length) {
fetchBasicGasAndTimeEstimates()
.then(({ blockTime }) => fetchGasEstimates(blockTime))
}
}
componentDidUpdate (prevProps) {
const { pendingTransactions: prevPendingTransactions = [] } = prevProps
const { pendingTransactions = [], updateNetworkNonce } = this.props
const {
pendingTransactions = [],
updateNetworkNonce,
fetchBasicGasAndTimeEstimates,
fetchGasEstimates,
transactionTimeFeatureActive,
} = this.props
if (pendingTransactions.length > prevPendingTransactions.length) {
updateNetworkNonce()
}
const transactionTimeFeatureWasActivated = !prevProps.transactionTimeFeatureActive && transactionTimeFeatureActive
const pendingTransactionAdded = pendingTransactions.length > 0 && prevPendingTransactions.length === 0
if (transactionTimeFeatureActive && pendingTransactions.length > 0 && (transactionTimeFeatureWasActivated || pendingTransactionAdded)) {
fetchBasicGasAndTimeEstimates()
.then(({ blockTime }) => fetchGasEstimates(blockTime))
}
}
shouldShowSpeedUp = (transactionGroup, isEarliestNonce) => {
@ -87,7 +118,7 @@ export default class TransactionList extends PureComponent {
}
renderTransaction (transactionGroup, index, isPendingTx = false) {
const { selectedToken, assetImages } = this.props
const { selectedToken, assetImages, firstPendingTransactionId } = this.props
const { transactions = [] } = transactionGroup
return transactions[0].key === TRANSACTION_TYPE_SHAPESHIFT
@ -105,6 +136,7 @@ export default class TransactionList extends PureComponent {
isEarliestNonce={isPendingTx && index === 0}
token={selectedToken}
assetImages={assetImages}
firstPendingTransactionId={firstPendingTransactionId}
/>
)
}

@ -6,23 +6,30 @@ import {
nonceSortedCompletedTransactionsSelector,
nonceSortedPendingTransactionsSelector,
} from '../../../selectors/transactions'
import { getSelectedAddress, getAssetImages } from '../../../selectors/selectors'
import { getSelectedAddress, getAssetImages, getFeatureFlags } from '../../../selectors/selectors'
import { selectedTokenSelector } from '../../../selectors/tokens'
import { updateNetworkNonce } from '../../../store/actions'
import { fetchBasicGasAndTimeEstimates, fetchGasEstimates } from '../../../ducks/gas/gas.duck'
const mapStateToProps = state => {
const mapStateToProps = (state) => {
const pendingTransactions = nonceSortedPendingTransactionsSelector(state)
const firstPendingTransactionId = pendingTransactions[0] && pendingTransactions[0].primaryTransaction.id
return {
completedTransactions: nonceSortedCompletedTransactionsSelector(state),
pendingTransactions: nonceSortedPendingTransactionsSelector(state),
pendingTransactions,
firstPendingTransactionId,
selectedToken: selectedTokenSelector(state),
selectedAddress: getSelectedAddress(state),
assetImages: getAssetImages(state),
transactionTimeFeatureActive: getFeatureFlags(state).transactionTime,
}
}
const mapDispatchToProps = dispatch => {
return {
updateNetworkNonce: address => dispatch(updateNetworkNonce(address)),
fetchGasEstimates: (blockTime) => dispatch(fetchGasEstimates(blockTime)),
fetchBasicGasAndTimeEstimates: () => dispatch(fetchBasicGasAndTimeEstimates()),
}
}

@ -0,0 +1 @@
export { default } from './transaction-time-remaining.container'

@ -0,0 +1,52 @@
import React, { PureComponent } from 'react'
import PropTypes from 'prop-types'
import { calcTransactionTimeRemaining } from './transaction-time-remaining.util'
export default class TransactionTimeRemaining extends PureComponent {
static propTypes = {
className: PropTypes.string,
initialTimeEstimate: PropTypes.number,
submittedTime: PropTypes.number,
}
constructor (props) {
super(props)
const { initialTimeEstimate, submittedTime } = props
this.state = {
timeRemaining: calcTransactionTimeRemaining(initialTimeEstimate, submittedTime),
}
this.interval = setInterval(
() => this.setState({ timeRemaining: calcTransactionTimeRemaining(initialTimeEstimate, submittedTime) }),
1000
)
}
componentDidUpdate (prevProps) {
const { initialTimeEstimate, submittedTime } = this.props
if (initialTimeEstimate !== prevProps.initialTimeEstimate) {
clearInterval(this.interval)
const calcedTimeRemaining = calcTransactionTimeRemaining(initialTimeEstimate, submittedTime)
this.setState({ timeRemaining: calcedTimeRemaining })
this.interval = setInterval(
() => this.setState({ timeRemaining: calcTransactionTimeRemaining(initialTimeEstimate, submittedTime) }),
1000
)
}
}
componentWillUnmount () {
clearInterval(this.interval)
}
render () {
const { className } = this.props
const { timeRemaining } = this.state
return (
<div className={className}>
{ timeRemaining }
</div>
)
}
}

@ -0,0 +1,41 @@
import { connect } from 'react-redux'
import { withRouter } from 'react-router-dom'
import { compose } from 'recompose'
import TransactionTimeRemaining from './transaction-time-remaining.component'
import {
getTxParams,
} from '../../../selectors/transactions'
import {
getEstimatedGasPrices,
getEstimatedGasTimes,
} from '../../../selectors/custom-gas'
import { getRawTimeEstimateData } from '../../../helpers/utils/gas-time-estimates.util'
import { hexWEIToDecGWEI } from '../../../helpers/utils/conversions.util'
const mapStateToProps = (state, ownProps) => {
const { transaction } = ownProps
const { gasPrice: currentGasPrice } = getTxParams(state, transaction)
const customGasPrice = calcCustomGasPrice(currentGasPrice)
const gasPrices = getEstimatedGasPrices(state)
const estimatedTimes = getEstimatedGasTimes(state)
const {
newTimeEstimate: initialTimeEstimate,
} = getRawTimeEstimateData(customGasPrice, gasPrices, estimatedTimes)
const submittedTime = transaction.submittedTime
return {
initialTimeEstimate,
submittedTime,
}
}
export default compose(
withRouter,
connect(mapStateToProps)
)(TransactionTimeRemaining)
function calcCustomGasPrice (customGasPriceInHex) {
return Number(hexWEIToDecGWEI(customGasPriceInHex))
}

@ -0,0 +1,13 @@
import { formatTimeEstimate } from '../../../helpers/utils/gas-time-estimates.util'
export function calcTransactionTimeRemaining (initialTimeEstimate, submittedTime) {
const currentTime = (new Date()).getTime()
const timeElapsedSinceSubmission = (currentTime - submittedTime) / 1000
const timeRemainingOnEstimate = initialTimeEstimate - timeElapsedSinceSubmission
const renderingTimeRemainingEstimate = timeRemainingOnEstimate < 30
? '< 30 s'
: formatTimeEstimate(timeRemainingOnEstimate)
return renderingTimeRemainingEstimate
}

@ -5,7 +5,7 @@ import Identicon from '../identicon'
import Tooltip from '../tooltip-v2'
import copyToClipboard from 'copy-to-clipboard'
import { DEFAULT_VARIANT, CARDS_VARIANT, FLAT_VARIANT } from './sender-to-recipient.constants'
import { checksumAddress } from '../../../helpers/utils/util'
import { checksumAddress, addressSlicer } from '../../../helpers/utils/util'
const variantHash = {
[DEFAULT_VARIANT]: 'sender-to-recipient--default',
@ -18,6 +18,7 @@ export default class SenderToRecipient extends PureComponent {
senderName: PropTypes.string,
senderAddress: PropTypes.string,
recipientName: PropTypes.string,
recipientEns: PropTypes.string,
recipientAddress: PropTypes.string,
recipientNickname: PropTypes.string,
t: PropTypes.func,
@ -60,14 +61,28 @@ export default class SenderToRecipient extends PureComponent {
return (
<Tooltip
position="bottom"
title={this.state.senderAddressCopied ? t('copiedExclamation') : t('copyAddress')}
html={
this.state.senderAddressCopied
? <p>{t('copiedExclamation')}</p>
: addressOnly
? <p>{t('copyAddress')}</p>
: (
<p>
{addressSlicer(checksummedSenderAddress)}<br/>
{t('copyAddress')}
</p>
)
}
wrapperClassName="sender-to-recipient__tooltip-wrapper"
containerClassName="sender-to-recipient__tooltip-container"
onHidden={() => this.setState({ senderAddressCopied: false })}
>
<div className="sender-to-recipient__name">
<span>{ addressOnly ? `${t('from')}: ` : '' }</span>
{ addressOnly ? checksummedSenderAddress : senderName }
{
addressOnly
? <span>{`${t('from')}: ${checksummedSenderAddress}`}</span>
: senderName
}
</div>
</Tooltip>
)
@ -90,7 +105,7 @@ export default class SenderToRecipient extends PureComponent {
renderRecipientWithAddress () {
const { t } = this.context
const { recipientName, recipientAddress, recipientNickname, addressOnly, onRecipientClick } = this.props
const { recipientEns, recipientName, recipientAddress, recipientNickname, addressOnly, onRecipientClick } = this.props
const checksummedRecipientAddress = checksumAddress(recipientAddress)
return (
@ -107,7 +122,18 @@ export default class SenderToRecipient extends PureComponent {
{ this.renderRecipientIdenticon() }
<Tooltip
position="bottom"
title={this.state.recipientAddressCopied ? t('copiedExclamation') : t('copyAddress')}
html={
this.state.senderAddressCopied
? <p>{t('copiedExclamation')}</p>
: (addressOnly && !recipientNickname && !recipientEns)
? <p>{t('copyAddress')}</p>
: (
<p>
{addressSlicer(checksummedRecipientAddress)}<br/>
{t('copyAddress')}
</p>
)
}
wrapperClassName="sender-to-recipient__tooltip-wrapper"
containerClassName="sender-to-recipient__tooltip-container"
onHidden={() => this.setState({ recipientAddressCopied: false })}
@ -116,8 +142,8 @@ export default class SenderToRecipient extends PureComponent {
<span>{ addressOnly ? `${t('to')}: ` : '' }</span>
{
addressOnly
? checksummedRecipientAddress
: (recipientNickname || recipientName || this.context.t('newContract'))
? (recipientNickname || recipientEns || checksummedRecipientAddress)
: (recipientNickname || recipientEns || recipientName || this.context.t('newContract'))
}
</div>
</Tooltip>

@ -8,6 +8,7 @@ export default class Tooltip extends PureComponent {
children: null,
containerClassName: '',
hideOnClick: false,
html: null,
onHidden: null,
position: 'left',
size: 'small',
@ -21,6 +22,7 @@ export default class Tooltip extends PureComponent {
children: PropTypes.node,
containerClassName: PropTypes.string,
disabled: PropTypes.bool,
html: PropTypes.node,
onHidden: PropTypes.func,
position: PropTypes.oneOf([
'top',
@ -38,9 +40,9 @@ export default class Tooltip extends PureComponent {
}
render () {
const {arrow, children, containerClassName, disabled, position, size, title, trigger, onHidden, wrapperClassName, style } = this.props
const {arrow, children, containerClassName, disabled, position, html, size, title, trigger, onHidden, wrapperClassName, style } = this.props
if (!title) {
if (!title && !html) {
return (
<div className={wrapperClassName}>
{children}
@ -51,6 +53,7 @@ export default class Tooltip extends PureComponent {
return (
<div className={wrapperClassName}>
<ReactTippy
html={html}
className={containerClassName}
disabled={disabled}
title={title}

@ -141,11 +141,11 @@
}
.cursor-pointer:hover {
transform: scale(1.1);
transform: scale(1.05);
}
.cursor-pointer:active {
transform: scale(.95);
transform: scale(.97);
}
.cursor-disabled {

@ -15,6 +15,7 @@ export default function withTokenTracker (WrappedComponent) {
this.state = {
string: '',
symbol: '',
balance: '',
error: null,
}
@ -78,8 +79,8 @@ export default function withTokenTracker (WrappedComponent) {
if (!this.tracker.running) {
return
}
const [{ string, symbol }] = tokens
this.setState({ string, symbol, error: null })
const [{ string, symbol, balance }] = tokens
this.setState({ string, symbol, error: null, balance })
}
removeListeners () {
@ -91,13 +92,13 @@ export default function withTokenTracker (WrappedComponent) {
}
render () {
const { string, symbol, error } = this.state
const { balance, string, symbol, error } = this.state
return (
<WrappedComponent
{ ...this.props }
string={string}
symbol={symbol}
tokenTrackerBalance={balance}
error={error}
/>
)

@ -0,0 +1,99 @@
import BigNumber from 'bignumber.js'
export function newBigSigDig (n) {
return new BigNumber((new BigNumber(String(n))).toPrecision(15))
}
const createOp = (a, b, op) => (newBigSigDig(a))[op](newBigSigDig(b))
export function bigNumMinus (a = 0, b = 0) {
return createOp(a, b, 'minus')
}
export function bigNumDiv (a = 0, b = 1) {
return createOp(a, b, 'div')
}
export function extrapolateY ({ higherY = 0, lowerY = 0, higherX = 0, lowerX = 0, xForExtrapolation = 0 }) {
const slope = bigNumMinus(higherY, lowerY).div(bigNumMinus(higherX, lowerX))
const newTimeEstimate = slope.times(bigNumMinus(higherX, xForExtrapolation)).minus(newBigSigDig(higherY)).negated()
return newTimeEstimate.toNumber()
}
export function getAdjacentGasPrices ({ gasPrices, priceToPosition }) {
const closestLowerValueIndex = gasPrices.findIndex((e, i, a) => e <= priceToPosition && a[i + 1] >= priceToPosition)
const closestHigherValueIndex = gasPrices.findIndex((e) => e > priceToPosition)
return {
closestLowerValueIndex,
closestHigherValueIndex,
closestHigherValue: gasPrices[closestHigherValueIndex],
closestLowerValue: gasPrices[closestLowerValueIndex],
}
}
export function formatTimeEstimate (totalSeconds, greaterThanMax, lessThanMin) {
const minutes = Math.floor(totalSeconds / 60)
const seconds = Math.floor(totalSeconds % 60)
if (!minutes && !seconds) {
return '...'
}
let symbol = '~'
if (greaterThanMax) {
symbol = '< '
} else if (lessThanMin) {
symbol = '> '
}
const formattedMin = `${minutes ? minutes + ' min' : ''}`
const formattedSec = `${seconds ? seconds + ' sec' : ''}`
const formattedCombined = formattedMin && formattedSec
? `${symbol}${formattedMin} ${formattedSec}`
: symbol + (formattedMin || formattedSec)
return formattedCombined
}
export function getRawTimeEstimateData (currentGasPrice, gasPrices, estimatedTimes) {
const minGasPrice = gasPrices[0]
const maxGasPrice = gasPrices[gasPrices.length - 1]
let priceForEstimation = currentGasPrice
if (currentGasPrice < minGasPrice) {
priceForEstimation = minGasPrice
} else if (currentGasPrice > maxGasPrice) {
priceForEstimation = maxGasPrice
}
const {
closestLowerValueIndex,
closestHigherValueIndex,
closestHigherValue,
closestLowerValue,
} = getAdjacentGasPrices({ gasPrices, priceToPosition: priceForEstimation })
const newTimeEstimate = extrapolateY({
higherY: estimatedTimes[closestHigherValueIndex],
lowerY: estimatedTimes[closestLowerValueIndex],
higherX: closestHigherValue,
lowerX: closestLowerValue,
xForExtrapolation: priceForEstimation,
})
return {
newTimeEstimate,
minGasPrice,
maxGasPrice,
}
}
export function getRenderableTimeEstimate (currentGasPrice, gasPrices, estimatedTimes) {
const {
newTimeEstimate,
minGasPrice,
maxGasPrice,
} = getRawTimeEstimateData(currentGasPrice, gasPrices, estimatedTimes)
return formatTimeEstimate(newTimeEstimate, currentGasPrice > maxGasPrice, currentGasPrice < minGasPrice)
}

@ -128,6 +128,11 @@ export function calcTokenAmount (value, decimals) {
return new BigNumber(String(value)).div(multiplier)
}
export function calcTokenValue (value, decimals) {
const multiplier = Math.pow(10, Number(decimals || 0))
return new BigNumber(String(value)).times(multiplier)
}
export function getTokenValue (tokenParams = []) {
const valueData = tokenParams.find(param => param.name === '_value')
return valueData && valueData.value

@ -0,0 +1,223 @@
import React, { Component } from 'react'
import PropTypes from 'prop-types'
import classnames from 'classnames'
import Identicon from '../../../components/ui/identicon'
import {
addressSummary,
} from '../../../helpers/utils/util'
export default class ConfirmApproveContent extends Component {
static contextTypes = {
t: PropTypes.func,
}
static propTypes = {
amount: PropTypes.string,
txFeeTotal: PropTypes.string,
tokenAmount: PropTypes.string,
customTokenAmount: PropTypes.string,
tokenSymbol: PropTypes.string,
siteImage: PropTypes.string,
tokenAddress: PropTypes.string,
showCustomizeGasModal: PropTypes.func,
showEditApprovalPermissionModal: PropTypes.func,
origin: PropTypes.string,
setCustomAmount: PropTypes.func,
tokenBalance: PropTypes.string,
data: PropTypes.string,
toAddress: PropTypes.string,
fiatTransactionTotal: PropTypes.string,
ethTransactionTotal: PropTypes.string,
}
state = {
showFullTxDetails: false,
}
renderApproveContentCard ({
symbol,
title,
showEdit,
onEditClick,
content,
footer,
noBorder,
}) {
return (
<div className={classnames({
'confirm-approve-content__card': !noBorder,
'confirm-approve-content__card--no-border': noBorder,
})}>
<div className="confirm-approve-content__card-header">
<div className="confirm-approve-content__card-header__symbol">{ symbol }</div>
<div className="confirm-approve-content__card-header__title">{ title }</div>
{ showEdit && <div
className="confirm-approve-content__small-blue-text cursor-pointer"
onClick={() => onEditClick()}
>Edit</div> }
</div>
<div className="confirm-approve-content__card-content">
{ content }
</div>
{ footer }
</div>
)
}
// TODO: Add "Learn Why" with link to the feeAssociatedRequest text
renderTransactionDetailsContent () {
const { t } = this.context
const {
ethTransactionTotal,
fiatTransactionTotal,
} = this.props
return (
<div className="confirm-approve-content__transaction-details-content">
<div className="confirm-approve-content__small-text">
{ t('feeAssociatedRequest') }
</div>
<div className="confirm-approve-content__transaction-details-content__fee">
<div className="confirm-approve-content__transaction-details-content__primary-fee">
{ fiatTransactionTotal }
</div>
<div className="confirm-approve-content__transaction-details-content__secondary-fee">
{ ethTransactionTotal }
</div>
</div>
</div>
)
}
renderPermissionContent () {
const { t } = this.context
const { customTokenAmount, tokenAmount, tokenSymbol, origin, toAddress } = this.props
return (
<div className="flex-column">
<div className="confirm-approve-content__small-text">{ t('accessAndSpendNotice', [origin]) }</div>
<div className="flex-row">
<div className="confirm-approve-content__label">{ t('amountWithColon') }</div>
<div className="confirm-approve-content__medium-text">{ `${customTokenAmount || tokenAmount} ${tokenSymbol}` }</div>
</div>
<div className="flex-row">
<div className="confirm-approve-content__label">{ t('toWithColon') }</div>
<div className="confirm-approve-content__medium-text">{ addressSummary(toAddress) }</div>
</div>
</div>
)
}
renderDataContent () {
const { t } = this.context
const { data } = this.props
return (
<div className="flex-column">
<div className="confirm-approve-content__small-text">{ t('functionApprove') }</div>
<div className="confirm-approve-content__small-text confirm-approve-content__data__data-block">{ data }</div>
</div>
)
}
render () {
const { t } = this.context
const {
siteImage,
tokenAmount,
customTokenAmount,
origin,
tokenSymbol,
showCustomizeGasModal,
showEditApprovalPermissionModal,
setCustomAmount,
tokenBalance,
} = this.props
const { showFullTxDetails } = this.state
return (
<div className={classnames('confirm-approve-content', {
'confirm-approve-content--full': showFullTxDetails,
})}>
<div className="confirm-approve-content__identicon-wrapper">
<Identicon
className="confirm-approve-content__identicon"
diameter={48}
address={origin}
image={siteImage}
/>
</div>
<div className="confirm-approve-content__title">
{ t('allowOriginSpendToken', [origin, tokenSymbol]) }
</div>
<div className="confirm-approve-content__description">
{ t('trustSiteApprovePermission', [origin, tokenSymbol]) }
</div>
<div
className="confirm-approve-content__edit-submission-button-container"
>
<div
className="confirm-approve-content__medium-link-text cursor-pointer"
onClick={() => showEditApprovalPermissionModal({ customTokenAmount, tokenAmount, tokenSymbol, setCustomAmount, tokenBalance, origin })}
>
{ t('editPermission') }
</div>
</div>
<div className="confirm-approve-content__card-wrapper">
{this.renderApproveContentCard({
symbol: <i className="fa fa-tag" />,
title: 'Transaction Fee',
showEdit: true,
onEditClick: showCustomizeGasModal,
content: this.renderTransactionDetailsContent(),
noBorder: !showFullTxDetails,
footer: <div
className="confirm-approve-content__view-full-tx-button-wrapper"
onClick={() => this.setState({ showFullTxDetails: !this.state.showFullTxDetails })}
>
<div className="confirm-approve-content__view-full-tx-button cursor-pointer">
<div className="confirm-approve-content__small-blue-text">
View full transaction details
</div>
<i className={classnames({
'fa fa-caret-up': showFullTxDetails,
'fa fa-caret-down': !showFullTxDetails,
})} />
</div>
</div>,
})}
</div>
{
showFullTxDetails
? (
<div className="confirm-approve-content__full-tx-content">
<div className="confirm-approve-content__permission">
{this.renderApproveContentCard({
symbol: <img src="/images/user-check.svg" />,
title: 'Permission',
content: this.renderPermissionContent(),
showEdit: true,
onEditClick: () => showEditApprovalPermissionModal({
customTokenAmount,
tokenAmount,
tokenSymbol,
tokenBalance,
setCustomAmount,
}),
})}
</div>
<div className="confirm-approve-content__data">
{this.renderApproveContentCard({
symbol: <i className="fa fa-file" />,
title: 'Data',
content: this.renderDataContent(),
noBorder: true,
})}
</div>
</div>
)
: null
}
</div>
)
}
}

@ -0,0 +1 @@
export { default } from './confirm-approve-content.component'

@ -0,0 +1,306 @@
.confirm-approve-content {
display: flex;
flex-flow: column;
align-items: center;
width: 100%;
height: 100%;
font-family: Roboto;
font-style: normal;
&__identicon-wrapper {
display: flex;
width: 100%;
justify-content: center;
margin-top: 22px;
padding-left: 24px;
padding-right: 24px;
}
&__full-tx-content {
display: flex;
flex-flow: column;
align-items: center;
width: 390px;
font-family: Roboto;
font-style: normal;
padding-left: 24px;
padding-right: 24px;
}
&__card-wrapper {
width: 100%;
}
&__title {
font-weight: normal;
font-size: 24px;
line-height: 34px;
width: 100%;
display: flex;
justify-content: center;
text-align: center;
margin-top: 22px;
padding-left: 24px;
padding-right: 24px;
}
&__description {
font-weight: normal;
font-size: 14px;
line-height: 20px;
margin-top: 16px;
margin-bottom: 16px;
color: #6A737D;
text-align: center;
padding-left: 24px;
padding-right: 24px;
}
&__card,
&__card--no-border {
display: flex;
flex-flow: column;
border-bottom: 1px solid #D2D8DD;
position: relative;
padding-left: 24px;
padding-right: 24px;
&__bold-text {
font-weight: bold;
font-size: 14px;
line-height: 20px;
}
&__thin-text {
font-weight: normal;
font-size: 12px;
line-height: 17px;
color: #6A737D;
}
}
&__card--no-border {
border-bottom: none;
}
&__card-header {
display: flex;
flex-flow: row;
margin-top: 20px;
align-items: center;
position: relative;
&__symbol {
width: auto;
}
&__symbol--aligned {
width: 100%;
}
&__title, &__title-value {
font-weight: bold;
font-size: 14px;
line-height: 20px;
}
&__title {
width: 100%;
margin-left: 16px;
}
&__title--aligned {
margin-left: 27px;
position: absolute;
width: auto;
}
}
&__card-content {
margin-top: 6px;
margin-bottom: 12px;
}
&__card-content--aligned {
margin-left: 42px;
}
&__transaction-total-symbol {
width: 16px;
display: flex;
justify-content: center;
align-items: center;
height: 16px;
&__x {
display: flex;
justify-content: center;
align-items: center;
div {
width: 22px;
height: 2px;
background: #037DD6;
position: absolute;
}
div:first-of-type {
transform: rotate(45deg);
}
div:last-of-type {
transform: rotate(-45deg);
}
}
&__circle {
width: 14px;
height: 14px;
border: 2px solid #037DD6;
border-radius: 50%;
background: white;
position: absolute;
}
}
&__transaction-details-content {
display: flex;
flex-flow: row;
justify-content: space-between;
.confirm-approve-content__small-text {
width: 160px;
}
&__fee {
display: flex;
flex-flow: column;
align-items: flex-end;
text-align: right;
}
&__primary-fee {
font-weight: bold;
font-size: 18px;
line-height: 25px;
color: #000000;
}
&__secondary-fee {
font-weight: normal;
font-size: 14px;
line-height: 20px;
color: #8C8E94;
}
}
&__view-full-tx-button-wrapper {
display: flex;
flex-flow: row;
margin-bottom: 16px;
justify-content: center;
i {
margin-left: 6px;
display: flex;
color: #3099f2;
align-items: center;
}
}
&__view-full-tx-button {
display: flex;
flex-flow: row;
}
&__edit-submission-button-container {
display: flex;
flex-flow: row;
padding-top: 15px;
padding-bottom: 30px;
border-bottom: 1px solid #D2D8DD;
width: 100%;
justify-content: center;
padding-left: 24px;
padding-right: 24px;
}
&__large-text {
font-size: 18px;
line-height: 25px;
color: #24292E;
}
&__medium-link-text {
font-size: 14px;
line-height: 20px;
font-weight: 500;
color: #037DD6;
}
&__medium-text,
&__label {
font-weight: normal;
font-size: 14px;
line-height: 20px;
color: #24292E;
}
&__label {
font-weight: bold;
margin-right: 4px;
}
&__small-text, &__small-blue-text, &__info-row {
font-weight: normal;
font-size: 12px;
line-height: 17px;
color: #6A737D;
}
&__small-blue-text {
color: #037DD6;
}
&__info-row {
display: flex;
justify-content: space-between;
margin-bottom: 6px;
}
&__data,
&__permission {
width: 100%;
}
&__permission {
.flex-row {
margin-top: 14px;
}
}
&__data {
&__data-block {
overflow-wrap: break-word;
margin-right: 16px;
margin-top: 12px;
}
}
&__footer {
display: flex;
align-items: flex-end;
margin-top: 16px;
padding-left: 34px;
padding-right: 24px;
.confirm-approve-content__small-text {
margin-left: 16px;
}
}
}
.confirm-approve-content--full {
height: auto;
}

@ -1,20 +1,109 @@
import React, { Component } from 'react'
import PropTypes from 'prop-types'
import ConfirmTokenTransactionBase from '../confirm-token-transaction-base'
import ConfirmTransactionBase from '../confirm-transaction-base'
import ConfirmApproveContent from './confirm-approve-content'
import { getCustomTxParamsData } from './confirm-approve.util'
import {
calcTokenAmount,
} from '../../helpers/utils/token-util'
export default class ConfirmApprove extends Component {
static contextTypes = {
t: PropTypes.func,
}
static propTypes = {
tokenAddress: PropTypes.string,
toAddress: PropTypes.string,
tokenAmount: PropTypes.number,
tokenSymbol: PropTypes.string,
fiatTransactionTotal: PropTypes.string,
ethTransactionTotal: PropTypes.string,
contractExchangeRate: PropTypes.number,
conversionRate: PropTypes.number,
currentCurrency: PropTypes.string,
showCustomizeGasModal: PropTypes.func,
showEditApprovalPermissionModal: PropTypes.func,
origin: PropTypes.string,
siteImage: PropTypes.string,
tokenTrackerBalance: PropTypes.string,
data: PropTypes.string,
decimals: PropTypes.number,
txData: PropTypes.object,
}
static defaultProps = {
tokenAmount: 0,
}
state = {
customPermissionAmount: '',
}
componentDidUpdate (prevProps) {
const { tokenAmount } = this.props
if (tokenAmount !== prevProps.tokenAmount) {
this.setState({ customPermissionAmount: tokenAmount })
}
}
render () {
const { tokenAmount, tokenSymbol } = this.props
const {
toAddress,
tokenAddress,
tokenSymbol,
tokenAmount,
showCustomizeGasModal,
showEditApprovalPermissionModal,
origin,
siteImage,
tokenTrackerBalance,
data,
decimals,
txData,
ethTransactionTotal,
fiatTransactionTotal,
...restProps
} = this.props
const { customPermissionAmount } = this.state
const tokensText = `${tokenAmount} ${tokenSymbol}`
const tokenBalance = tokenTrackerBalance
? Number(calcTokenAmount(tokenTrackerBalance, decimals)).toPrecision(9)
: ''
return (
<ConfirmTokenTransactionBase
tokenAmount={tokenAmount}
warning={`By approving this action, you grant permission for this contract to spend up to ${tokenAmount} of your ${tokenSymbol}.`}
<ConfirmTransactionBase
toAddress={toAddress}
identiconAddress={tokenAddress}
showAccountInHeader={true}
title={tokensText}
contentComponent={<ConfirmApproveContent
siteImage={siteImage}
tokenAddress={tokenAddress}
setCustomAmount={(newAmount) => {
this.setState({ customPermissionAmount: newAmount })
}}
customTokenAmount={String(customPermissionAmount)}
tokenAmount={String(tokenAmount)}
origin={origin}
tokenSymbol={tokenSymbol}
tokenBalance={tokenBalance}
showCustomizeGasModal={() => showCustomizeGasModal(txData)}
showEditApprovalPermissionModal={showEditApprovalPermissionModal}
data={data}
toAddress={toAddress}
ethTransactionTotal={ethTransactionTotal}
fiatTransactionTotal={fiatTransactionTotal}
/>}
hideSenderToRecipient={true}
customTxParamsData={customPermissionAmount
? getCustomTxParamsData(data, { customPermissionAmount, tokenAmount, decimals })
: null
}
{...restProps}
/>
)
}

@ -1,15 +1,102 @@
import { connect } from 'react-redux'
import { compose } from 'recompose'
import { withRouter } from 'react-router-dom'
import {
contractExchangeRateSelector,
transactionFeeSelector,
} from '../../selectors/confirm-transaction'
import { showModal } from '../../store/actions'
import { tokenSelector } from '../../selectors/tokens'
import {
getTokenData,
} from '../../helpers/utils/transactions.util'
import withTokenTracker from '../../helpers/higher-order-components/with-token-tracker'
import {
calcTokenAmount,
getTokenToAddress,
getTokenValue,
} from '../../helpers/utils/token-util'
import ConfirmApprove from './confirm-approve.component'
import { approveTokenAmountAndToAddressSelector } from '../../selectors/confirm-transaction'
const mapStateToProps = state => {
const { confirmTransaction: { tokenProps: { tokenSymbol } = {} } } = state
const { tokenAmount } = approveTokenAmountAndToAddressSelector(state)
const mapStateToProps = (state, ownProps) => {
const { match: { params = {} } } = ownProps
const { id: paramsTransactionId } = params
const {
confirmTransaction,
metamask: { currentCurrency, conversionRate, selectedAddressTxList, approvedOrigins, selectedAddress },
} = state
const {
txData: { id: transactionId, txParams: { to: tokenAddress, data } = {} } = {},
} = confirmTransaction
const transaction = selectedAddressTxList.find(({ id }) => id === (Number(paramsTransactionId) || transactionId)) || {}
const {
ethTransactionTotal,
fiatTransactionTotal,
} = transactionFeeSelector(state, transaction)
const tokens = tokenSelector(state)
const currentToken = tokens && tokens.find(({ address }) => tokenAddress === address)
const { decimals, symbol: tokenSymbol } = currentToken || {}
const tokenData = getTokenData(data)
const tokenValue = tokenData && getTokenValue(tokenData.params)
const toAddress = tokenData && getTokenToAddress(tokenData.params)
const tokenAmount = tokenData && calcTokenAmount(tokenValue, decimals).toNumber()
const contractExchangeRate = contractExchangeRateSelector(state)
const { origin } = transaction
const formattedOrigin = origin
? origin[0].toUpperCase() + origin.slice(1)
: ''
const { siteImage } = approvedOrigins[origin] || {}
return {
toAddress,
tokenAddress,
tokenAmount,
currentCurrency,
conversionRate,
contractExchangeRate,
fiatTransactionTotal,
ethTransactionTotal,
tokenSymbol,
siteImage,
token: { address: tokenAddress },
userAddress: selectedAddress,
origin: formattedOrigin,
data,
decimals: Number(decimals),
txData: transaction,
}
}
export default connect(mapStateToProps)(ConfirmApprove)
const mapDispatchToProps = (dispatch) => {
return {
showCustomizeGasModal: (txData) => dispatch(showModal({ name: 'CUSTOMIZE_GAS', txData })),
showEditApprovalPermissionModal: ({
tokenAmount,
customTokenAmount,
tokenSymbol,
tokenBalance,
setCustomAmount,
origin,
}) => dispatch(showModal({
name: 'EDIT_APPROVAL_PERMISSION',
tokenAmount,
customTokenAmount,
tokenSymbol,
tokenBalance,
setCustomAmount,
origin,
})),
}
}
export default compose(
withRouter,
connect(mapStateToProps, mapDispatchToProps),
withTokenTracker,
)(ConfirmApprove)

@ -0,0 +1,28 @@
import { decimalToHex } from '../../helpers/utils/conversions.util'
import { calcTokenValue } from '../../helpers/utils/token-util.js'
export function getCustomTxParamsData (data, { customPermissionAmount, tokenAmount, decimals }) {
if (customPermissionAmount) {
const tokenValue = decimalToHex(calcTokenValue(tokenAmount, decimals))
const re = new RegExp('(^.+)' + tokenValue + '$')
const matches = re.exec(data)
if (!matches || !matches[1]) {
return data
}
let dataWithoutCurrentAmount = matches[1]
const customPermissionValue = decimalToHex(calcTokenValue(Number(customPermissionAmount), decimals))
const differenceInLengths = customPermissionValue.length - tokenValue.length
const zeroModifier = dataWithoutCurrentAmount.length - differenceInLengths
if (differenceInLengths > 0) {
dataWithoutCurrentAmount = dataWithoutCurrentAmount.slice(0, zeroModifier)
} else if (differenceInLengths < 0) {
dataWithoutCurrentAmount = dataWithoutCurrentAmount.padEnd(zeroModifier, 0)
}
const customTxParamsData = dataWithoutCurrentAmount + customPermissionValue
return customTxParamsData
}
}

@ -0,0 +1 @@
@import 'confirm-approve-content/index';

@ -64,6 +64,7 @@ export default class ConfirmTransactionBase extends Component {
tokenData: PropTypes.object,
tokenProps: PropTypes.object,
toName: PropTypes.string,
toEns: PropTypes.string,
toNickname: PropTypes.string,
transactionStatus: PropTypes.string,
txData: PropTypes.object,
@ -103,6 +104,9 @@ export default class ConfirmTransactionBase extends Component {
transactionCategory: PropTypes.string,
getNextNonce: PropTypes.func,
nextNonce: PropTypes.number,
tryReverseResolveAddress: PropTypes.func.isRequired,
hideSenderToRecipient: PropTypes.bool,
showAccountInHeader: PropTypes.bool,
}
state = {
@ -385,7 +389,8 @@ export default class ConfirmTransactionBase extends Component {
showRejectTransactionsConfirmationModal({
unapprovedTxCount,
async onSubmit () {
onSubmit: async () => {
this._removeBeforeUnload()
await cancelAllTransactions()
clearConfirmTransaction()
history.push(DEFAULT_ROUTE)
@ -407,6 +412,7 @@ export default class ConfirmTransactionBase extends Component {
updateCustomNonce,
} = this.props
this._removeBeforeUnload()
metricsEvent({
eventOpts: {
category: 'Transactions',
@ -456,6 +462,7 @@ export default class ConfirmTransactionBase extends Component {
submitting: true,
submitError: null,
}, () => {
this._removeBeforeUnload()
metricsEvent({
eventOpts: {
category: 'Transactions',
@ -566,8 +573,30 @@ export default class ConfirmTransactionBase extends Component {
}
}
_beforeUnload = () => {
const { txData: { origin, id } = {}, cancelTransaction } = this.props
const { metricsEvent } = this.context
metricsEvent({
eventOpts: {
category: 'Transactions',
action: 'Confirm Screen',
name: 'Cancel Tx Via Notification Close',
},
customVariables: {
origin,
},
})
cancelTransaction({ id })
}
_removeBeforeUnload = () => {
if (getEnvironmentType(window.location.href) === ENVIRONMENT_TYPE_NOTIFICATION) {
window.removeEventListener('beforeunload', this._beforeUnload)
}
}
componentDidMount () {
const { txData: { origin, id } = {}, cancelTransaction, getNextNonce } = this.props
const { toAddress, txData: { origin } = {}, getNextNonce, tryReverseResolveAddress } = this.props
const { metricsEvent } = this.context
metricsEvent({
eventOpts: {
@ -581,22 +610,15 @@ export default class ConfirmTransactionBase extends Component {
})
if (getEnvironmentType(window.location.href) === ENVIRONMENT_TYPE_NOTIFICATION) {
window.onbeforeunload = () => {
metricsEvent({
eventOpts: {
category: 'Transactions',
action: 'Confirm Screen',
name: 'Cancel Tx Via Notification Close',
},
customVariables: {
origin,
},
})
cancelTransaction({ id })
}
window.addEventListener('beforeunload', this._beforeUnload)
}
getNextNonce()
tryReverseResolveAddress(toAddress)
}
componentWillUnmount () {
this._removeBeforeUnload()
}
render () {
@ -606,6 +628,7 @@ export default class ConfirmTransactionBase extends Component {
fromAddress,
toName,
toAddress,
toEns,
toNickname,
methodData,
valid: propsValid = true,
@ -624,6 +647,8 @@ export default class ConfirmTransactionBase extends Component {
warning,
unapprovedTxCount,
transactionCategory,
hideSenderToRecipient,
showAccountInHeader,
} = this.props
const { submitting, submitError, submitWarning } = this.state
@ -634,8 +659,10 @@ export default class ConfirmTransactionBase extends Component {
<ConfirmPageContainer
fromName={fromName}
fromAddress={fromAddress}
showAccountInHeader={showAccountInHeader}
toName={toName}
toAddress={toAddress}
toEns={toEns}
toNickname={toNickname}
showEdit={onEdit && !isTxReprice}
// In the event that the key is falsy (and inherently invalid), use a fallback string
@ -671,6 +698,7 @@ export default class ConfirmTransactionBase extends Component {
onCancelAll={() => this.handleCancelAll()}
onCancel={() => this.handleCancel()}
onSubmit={() => this.handleSubmit()}
hideSenderToRecipient={hideSenderToRecipient}
/>
)
}

@ -18,6 +18,7 @@ import {
setMetaMetricsSendCount,
updateTransaction,
getNextNonce,
tryReverseResolveAddress,
} from '../../store/actions'
import {
INSUFFICIENT_FUNDS_ERROR_KEY,
@ -45,12 +46,13 @@ const customNonceMerge = txData => customNonceValue ? ({
}) : txData
const mapStateToProps = (state, ownProps) => {
const { toAddress: propsToAddress, match: { params = {} } } = ownProps
const { toAddress: propsToAddress, customTxParamsData, match: { params = {} } } = ownProps
const { id: paramsTransactionId } = params
const { showFiatInTestnets } = preferencesSelector(state)
const isMainnet = getIsMainnet(state)
const { confirmTransaction, metamask } = state
const {
ensResolutionsByAddress,
conversionRate,
identities,
addressBook,
@ -93,7 +95,9 @@ const mapStateToProps = (state, ownProps) => {
: addressSlicer(checksumAddress(toAddress))
)
const addressBookObject = addressBook[checksumAddress(toAddress)]
const checksummedAddress = checksumAddress(toAddress)
const addressBookObject = addressBook[checksummedAddress]
const toEns = ensResolutionsByAddress[checksummedAddress] || ''
const toNickname = addressBookObject ? addressBookObject.name : ''
const isTxReprice = Boolean(lastGasPrice)
const transactionStatus = transaction ? transaction.status : ''
@ -129,11 +133,23 @@ const mapStateToProps = (state, ownProps) => {
const methodData = getKnownMethodData(state, data) || {}
let fullTxData = { ...txData, ...transaction }
if (customTxParamsData) {
fullTxData = {
...fullTxData,
txParams: {
...fullTxData.txParams,
data: customTxParamsData,
},
}
}
return {
balance,
fromAddress,
fromName,
toAddress,
toEns,
toName,
toNickname,
ethTransactionAmount,
@ -145,7 +161,7 @@ const mapStateToProps = (state, ownProps) => {
hexTransactionAmount,
hexTransactionFee,
hexTransactionTotal,
txData: { ...txData, ...transaction },
txData: fullTxData,
tokenData,
methodData,
tokenProps,
@ -176,6 +192,9 @@ const mapStateToProps = (state, ownProps) => {
export const mapDispatchToProps = dispatch => {
return {
tryReverseResolveAddress: (address) => {
return dispatch(tryReverseResolveAddress(address))
},
updateCustomNonce: value => {
customNonceValue = value
dispatch(updateCustomNonce(value))

@ -9,7 +9,8 @@ const txHelper = require('../../../lib/tx-helper')
const log = require('loglevel')
const R = require('ramda')
const SignatureRequest = require('../../components/app/signature-request')
const SignatureRequest = require('../../components/app/signature-request').default
const SignatureRequestOriginal = require('../../components/app/signature-request-original')
const Loading = require('../../components/ui/loading-screen')
const { DEFAULT_ROUTE } = require('../../helpers/constants/routes')
@ -137,34 +138,45 @@ ConfirmTxScreen.prototype.getTxData = function () {
: unconfTxList[index]
}
ConfirmTxScreen.prototype.signatureSelect = function (type, version) {
// Temporarily direct only v3 and v4 requests to new code.
if (type === 'eth_signTypedData' && (version === 'V3' || version === 'V4')) {
return SignatureRequest
}
return SignatureRequestOriginal
}
ConfirmTxScreen.prototype.render = function () {
const props = this.props
const {
currentCurrency,
blockGasLimit,
conversionRate,
} = props
var txData = this.getTxData() || {}
const { msgParams } = txData
const { msgParams, type, msgParams: { version } } = txData
log.debug('msgParams detected, rendering pending msg')
return msgParams
? h(SignatureRequest, {
// Properties
txData: txData,
key: txData.id,
identities: props.identities,
currentCurrency,
blockGasLimit,
// Actions
signMessage: this.signMessage.bind(this, txData),
signPersonalMessage: this.signPersonalMessage.bind(this, txData),
signTypedMessage: this.signTypedMessage.bind(this, txData),
cancelMessage: this.cancelMessage.bind(this, txData),
cancelPersonalMessage: this.cancelPersonalMessage.bind(this, txData),
cancelTypedMessage: this.cancelTypedMessage.bind(this, txData),
})
: h(Loading)
return msgParams ? h(this.signatureSelect(type, version), {
// Properties
txData: txData,
key: txData.id,
selectedAddress: props.selectedAddress,
accounts: props.accounts,
identities: props.identities,
conversionRate,
currentCurrency,
blockGasLimit,
// Actions
signMessage: this.signMessage.bind(this, txData),
signPersonalMessage: this.signPersonalMessage.bind(this, txData),
signTypedMessage: this.signTypedMessage.bind(this, txData),
cancelMessage: this.cancelMessage.bind(this, txData),
cancelPersonalMessage: this.cancelPersonalMessage.bind(this, txData),
cancelTypedMessage: this.cancelTypedMessage.bind(this, txData),
}) : h(Loading)
}
ConfirmTxScreen.prototype.signMessage = function (msgData, event) {

@ -45,6 +45,7 @@ export default class ConfirmTransaction extends Component {
isTokenMethodAction: PropTypes.bool,
fullScreenVsPopupTestGroup: PropTypes.string,
trackABTest: PropTypes.bool,
conversionRate: PropTypes.number,
}
componentDidMount () {
@ -118,7 +119,6 @@ export default class ConfirmTransaction extends Component {
// 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 transactionId && (!paramsTransactionId || paramsTransactionId === transactionId)
? (
<Switch>

@ -25,6 +25,7 @@ const mapStateToProps = (state, ownProps) => {
send,
unapprovedTxs,
abTests: { fullScreenVsPopup },
conversionRate,
},
confirmTransaction,
} = state
@ -53,6 +54,7 @@ const mapStateToProps = (state, ownProps) => {
isTokenMethodAction: isTokenMethodAction(transactionCategory),
trackABTest,
fullScreenVsPopupTestGroup: fullScreenVsPopup,
conversionRate,
}
}

@ -50,7 +50,7 @@ export default class ImportWithSeedPhrase extends PureComponent {
}
componentWillMount () {
window.onbeforeunload = () => this.context.metricsEvent({
this._onBeforeUnload = () => this.context.metricsEvent({
eventOpts: {
category: 'Onboarding',
action: 'Import Seed Phrase',
@ -61,6 +61,11 @@ export default class ImportWithSeedPhrase extends PureComponent {
errorMessage: this.state.seedPhraseError,
},
})
window.addEventListener('beforeunload', this._onBeforeUnload)
}
componentWillUnmount () {
window.removeEventListener('beforeunload', this._onBeforeUnload)
}
handleSeedPhraseChange (seedPhrase) {

@ -120,7 +120,7 @@
&__button {
margin: 35px 0 14px;
width: 140px;
width: 170px;
height: 44px;
}

@ -42,7 +42,7 @@ export default class Home extends PureComponent {
selectedAddress: PropTypes.string,
restoreFromThreeBox: PropTypes.func,
setShowRestorePromptToFalse: PropTypes.func,
threeBoxLastUpdated: PropTypes.string,
threeBoxLastUpdated: PropTypes.number,
}
componentWillMount () {
@ -119,10 +119,10 @@ export default class Home extends PureComponent {
<TransactionView>
<MultipleNotifications
className
notifications={[
{
shouldBeRendered: showPrivacyModeNotification,
component: <HomeNotification
>
{
showPrivacyModeNotification
? <HomeNotification
descriptionText={t('privacyModeDefault')}
acceptText={t('learnMore')}
onAccept={() => {
@ -134,11 +134,12 @@ export default class Home extends PureComponent {
unsetMigratedPrivacyMode()
}}
key="home-privacyModeDefault"
/>,
},
{
shouldBeRendered: shouldShowSeedPhraseReminder,
component: <HomeNotification
/>
: null
}
{
shouldShowSeedPhraseReminder
? <HomeNotification
descriptionText={t('backupApprovalNotice')}
acceptText={t('backupNow')}
onAccept={() => {
@ -150,12 +151,13 @@ export default class Home extends PureComponent {
}}
infoText={t('backupApprovalInfo')}
key="home-backupApprovalNotice"
/>,
},
{
shouldBeRendered: threeBoxLastUpdated && showRestorePrompt,
component: <HomeNotification
descriptionText={t('restoreWalletPreferences', [ formatDate(parseInt(threeBoxLastUpdated), 'M/d/y') ])}
/>
: null
}
{
threeBoxLastUpdated && showRestorePrompt
? <HomeNotification
descriptionText={t('restoreWalletPreferences', [ formatDate(threeBoxLastUpdated, 'M/d/y') ])}
acceptText={t('restore')}
ignoreText={t('noThanks')}
infoText={t('dataBackupFoundInfo')}
@ -169,9 +171,10 @@ export default class Home extends PureComponent {
setShowRestorePromptToFalse()
}}
key="home-privacyModeDefault"
/>,
},
]}/>
/>
: null
}
</MultipleNotifications>
</TransactionView>
)
: null }

@ -11,3 +11,5 @@
@import 'first-time-flow/index';
@import 'keychains/index';
@import 'confirm-approve/index';

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save