mirror of
https://github.com/kremalicious/metamask-extension.git
synced 2024-12-23 09:52:26 +01:00
Merge pull request #7351 from MetaMask/Version-v7.5.0
Version v7.5.0 RC
This commit is contained in:
commit
c594bb340d
@ -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'
|
||||
|
18
CHANGELOG.md
18
CHANGELOG.md
@ -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"
|
||||
},
|
||||
|
3
app/images/user-check.svg
Normal file
3
app/images/user-check.svg
Normal file
@ -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__",
|
||||
|
25
app/scripts/controllers/ens/ens.js
Normal file
25
app/scripts/controllers/ens/ens.js
Normal file
@ -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
|
94
app/scripts/controllers/ens/index.js
Normal file
94
app/scripts/controllers/ens/index.js
Normal file
@ -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)
|
||||
})
|
||||
}
|
||||
|
28
app/scripts/controllers/network/middleware/pending.js
Normal file
28
app/scripts/controllers/network/middleware/pending.js
Normal file
@ -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)
|
||||
|
135
test/unit/app/controllers/ens-controller-test.js
Normal file
135
test/unit/app/controllers/ens-controller-test.js
Normal file
@ -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
|
81
test/unit/app/controllers/network/pending-middleware-test.js
Normal file
81
test/unit/app/controllers/network/pending-middleware-test.js
Normal file
@ -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()
|
||||
})
|
||||
})
|
||||
|
||||
})
|
225
test/unit/app/controllers/network/stubs.js
Normal file
225
test/unit/app/controllers/network/stubs.js
Normal file
@ -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'
|
167
ui/app/components/app/modals/edit-approval-permission/index.scss
Normal file
167
ui/app/components/app/modals/edit-approval-permission/index.scss
Normal file
@ -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: {
|
1
ui/app/components/app/signature-request/index.js
Normal file
1
ui/app/components/app/signature-request/index.js
Normal file
@ -0,0 +1 @@
|
||||
export { default } from './signature-request.container'
|
96
ui/app/components/app/signature-request/index.scss
Normal file
96
ui/app/components/app/signature-request/index.scss
Normal file
@ -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}
|
||||
/>
|
||||
)
|
||||
|
99
ui/app/helpers/utils/gas-time-estimates.util.js
Normal file
99
ui/app/helpers/utils/gas-time-estimates.util.js
Normal file
@ -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'
|
306
ui/app/pages/confirm-approve/confirm-approve-content/index.scss
Normal file
306
ui/app/pages/confirm-approve/confirm-approve-content/index.scss
Normal file
@ -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)
|
||||
|
||||
|
28
ui/app/pages/confirm-approve/confirm-approve.util.js
Normal file
28
ui/app/pages/confirm-approve/confirm-approve.util.js
Normal file
@ -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
|
||||
}
|
||||
}
|
1
ui/app/pages/confirm-approve/index.scss
Normal file
1
ui/app/pages/confirm-approve/index.scss
Normal file
@ -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…
x
Reference in New Issue
Block a user