Merge pull request #7351 from MetaMask/Version-v7.5.0
Version v7.5.0 RCfeature/default_network_editable
commit
c594bb340d
After Width: | Height: | Size: 769 B |
@ -0,0 +1,25 @@ |
||||
const EthJsEns = require('ethjs-ens') |
||||
const ensNetworkMap = require('ethjs-ens/lib/network-map.json') |
||||
|
||||
class Ens { |
||||
static getNetworkEnsSupport (network) { |
||||
return Boolean(ensNetworkMap[network]) |
||||
} |
||||
|
||||
constructor ({ network, provider } = {}) { |
||||
this._ethJsEns = new EthJsEns({ |
||||
network, |
||||
provider, |
||||
}) |
||||
} |
||||
|
||||
lookup (ensName) { |
||||
return this._ethJsEns.lookup(ensName) |
||||
} |
||||
|
||||
reverse (address) { |
||||
return this._ethJsEns.reverse(address) |
||||
} |
||||
} |
||||
|
||||
module.exports = Ens |
@ -0,0 +1,94 @@ |
||||
const ethUtil = require('ethereumjs-util') |
||||
const ObservableStore = require('obs-store') |
||||
const punycode = require('punycode') |
||||
const log = require('loglevel') |
||||
const Ens = require('./ens') |
||||
|
||||
const ZERO_ADDRESS = '0x0000000000000000000000000000000000000000' |
||||
const ZERO_X_ERROR_ADDRESS = '0x' |
||||
|
||||
class EnsController { |
||||
constructor ({ ens, provider, networkStore } = {}) { |
||||
const initState = { |
||||
ensResolutionsByAddress: {}, |
||||
} |
||||
|
||||
this._ens = ens |
||||
if (!this._ens) { |
||||
const network = networkStore.getState() |
||||
if (Ens.getNetworkEnsSupport(network)) { |
||||
this._ens = new Ens({ |
||||
network, |
||||
provider, |
||||
}) |
||||
} |
||||
} |
||||
|
||||
this.store = new ObservableStore(initState) |
||||
networkStore.subscribe((network) => { |
||||
this.store.putState(initState) |
||||
if (Ens.getNetworkEnsSupport(network)) { |
||||
this._ens = new Ens({ |
||||
network, |
||||
provider, |
||||
}) |
||||
} else { |
||||
delete this._ens |
||||
} |
||||
}) |
||||
} |
||||
|
||||
reverseResolveAddress (address) { |
||||
return this._reverseResolveAddress(ethUtil.toChecksumAddress(address)) |
||||
} |
||||
|
||||
async _reverseResolveAddress (address) { |
||||
if (!this._ens) { |
||||
return undefined |
||||
} |
||||
|
||||
const state = this.store.getState() |
||||
if (state.ensResolutionsByAddress[address]) { |
||||
return state.ensResolutionsByAddress[address] |
||||
} |
||||
|
||||
let domain |
||||
try { |
||||
domain = await this._ens.reverse(address) |
||||
} catch (error) { |
||||
log.debug(error) |
||||
return undefined |
||||
} |
||||
|
||||
let registeredAddress |
||||
try { |
||||
registeredAddress = await this._ens.lookup(domain) |
||||
} catch (error) { |
||||
log.debug(error) |
||||
return undefined |
||||
} |
||||
|
||||
if (registeredAddress === ZERO_ADDRESS || registeredAddress === ZERO_X_ERROR_ADDRESS) { |
||||
return undefined |
||||
} |
||||
|
||||
if (ethUtil.toChecksumAddress(registeredAddress) !== address) { |
||||
return undefined |
||||
} |
||||
|
||||
this._updateResolutionsByAddress(address, punycode.toASCII(domain)) |
||||
return domain |
||||
} |
||||
|
||||
_updateResolutionsByAddress (address, domain) { |
||||
const oldState = this.store.getState() |
||||
this.store.putState({ |
||||
ensResolutionsByAddress: { |
||||
...oldState.ensResolutionsByAddress, |
||||
[address]: domain, |
||||
}, |
||||
}) |
||||
} |
||||
} |
||||
|
||||
module.exports = EnsController |
@ -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, |
||||
} |
@ -0,0 +1,135 @@ |
||||
const assert = require('assert') |
||||
const sinon = require('sinon') |
||||
const ObservableStore = require('obs-store') |
||||
const HttpProvider = require('ethjs-provider-http') |
||||
const EnsController = require('../../../../app/scripts/controllers/ens') |
||||
|
||||
const ZERO_ADDRESS = '0x0000000000000000000000000000000000000000' |
||||
const ZERO_X_ERROR_ADDRESS = '0x' |
||||
|
||||
describe('EnsController', function () { |
||||
describe('#constructor', function () { |
||||
it('should construct the controller given a provider and a network', async () => { |
||||
const provider = new HttpProvider('https://ropsten.infura.io') |
||||
const currentNetworkId = '3' |
||||
const networkStore = new ObservableStore(currentNetworkId) |
||||
const ens = new EnsController({ |
||||
provider, |
||||
networkStore, |
||||
}) |
||||
|
||||
assert.ok(ens._ens) |
||||
}) |
||||
|
||||
it('should construct the controller given an existing ENS instance', async () => { |
||||
const networkStore = { |
||||
subscribe: sinon.spy(), |
||||
} |
||||
const ens = new EnsController({ |
||||
ens: {}, |
||||
networkStore, |
||||
}) |
||||
|
||||
assert.ok(ens._ens) |
||||
}) |
||||
}) |
||||
|
||||
describe('#reverseResolveName', function () { |
||||
it('should resolve to an ENS name', async () => { |
||||
const address = '0x8e5d75d60224ea0c33d0041e75de68b1c3cb6dd5' |
||||
const networkStore = { |
||||
subscribe: sinon.spy(), |
||||
} |
||||
const ens = new EnsController({ |
||||
ens: { |
||||
reverse: sinon.stub().withArgs(address).returns('peaksignal.eth'), |
||||
lookup: sinon.stub().withArgs('peaksignal.eth').returns(address), |
||||
}, |
||||
networkStore, |
||||
}) |
||||
|
||||
const name = await ens.reverseResolveAddress(address) |
||||
assert.equal(name, 'peaksignal.eth') |
||||
}) |
||||
|
||||
it('should only resolve an ENS name once', async () => { |
||||
const address = '0x8e5d75d60224ea0c33d0041e75de68b1c3cb6dd5' |
||||
const reverse = sinon.stub().withArgs(address).returns('peaksignal.eth') |
||||
const lookup = sinon.stub().withArgs('peaksignal.eth').returns(address) |
||||
const networkStore = { |
||||
subscribe: sinon.spy(), |
||||
} |
||||
const ens = new EnsController({ |
||||
ens: { |
||||
reverse, |
||||
lookup, |
||||
}, |
||||
networkStore, |
||||
}) |
||||
|
||||
assert.equal(await ens.reverseResolveAddress(address), 'peaksignal.eth') |
||||
assert.equal(await ens.reverseResolveAddress(address), 'peaksignal.eth') |
||||
assert.ok(lookup.calledOnce) |
||||
assert.ok(reverse.calledOnce) |
||||
}) |
||||
|
||||
it('should fail if the name is registered to a different address than the reverse-resolved', async () => { |
||||
const address = '0x8e5d75d60224ea0c33d0041e75de68b1c3cb6dd5' |
||||
const networkStore = { |
||||
subscribe: sinon.spy(), |
||||
} |
||||
const ens = new EnsController({ |
||||
ens: { |
||||
reverse: sinon.stub().withArgs(address).returns('peaksignal.eth'), |
||||
lookup: sinon.stub().withArgs('peaksignal.eth').returns('0xfoo'), |
||||
}, |
||||
networkStore, |
||||
}) |
||||
|
||||
const name = await ens.reverseResolveAddress(address) |
||||
assert.strictEqual(name, undefined) |
||||
}) |
||||
|
||||
it('should throw an error when the lookup resolves to the zero address', async () => { |
||||
const address = '0x8e5d75d60224ea0c33d0041e75de68b1c3cb6dd5' |
||||
const networkStore = { |
||||
subscribe: sinon.spy(), |
||||
} |
||||
const ens = new EnsController({ |
||||
ens: { |
||||
reverse: sinon.stub().withArgs(address).returns('peaksignal.eth'), |
||||
lookup: sinon.stub().withArgs('peaksignal.eth').returns(ZERO_ADDRESS), |
||||
}, |
||||
networkStore, |
||||
}) |
||||
|
||||
try { |
||||
await ens.reverseResolveAddress(address) |
||||
assert.fail('#reverseResolveAddress did not throw') |
||||
} catch (e) { |
||||
assert.ok(e) |
||||
} |
||||
}) |
||||
|
||||
it('should throw an error the lookup resolves to the zero x address', async () => { |
||||
const address = '0x8e5d75d60224ea0c33d0041e75de68b1c3cb6dd5' |
||||
const networkStore = { |
||||
subscribe: sinon.spy(), |
||||
} |
||||
const ens = new EnsController({ |
||||
ens: { |
||||
reverse: sinon.stub().withArgs(address).returns('peaksignal.eth'), |
||||
lookup: sinon.stub().withArgs('peaksignal.eth').returns(ZERO_X_ERROR_ADDRESS), |
||||
}, |
||||
networkStore, |
||||
}) |
||||
|
||||
try { |
||||
await ens.reverseResolveAddress(address) |
||||
assert.fail('#reverseResolveAddress did not throw') |
||||
} catch (e) { |
||||
assert.ok(e) |
||||
} |
||||
}) |
||||
}) |
||||
}) |
@ -1,9 +1,9 @@ |
||||
const assert = require('assert') |
||||
const nock = require('nock') |
||||
const NetworkController = require('../../../../app/scripts/controllers/network') |
||||
const NetworkController = require('../../../../../app/scripts/controllers/network') |
||||
const { |
||||
getNetworkDisplayName, |
||||
} = require('../../../../app/scripts/controllers/network/util') |
||||
} = require('../../../../../app/scripts/controllers/network/util') |
||||
|
||||
describe('# Network Controller', function () { |
||||
let networkController |
@ -0,0 +1,81 @@ |
||||
const assert = require('assert') |
||||
const { createPendingNonceMiddleware, createPendingTxMiddleware } = require('../../../../../app/scripts/controllers/network/middleware/pending') |
||||
const txMetaStub = require('./stubs').txMetaStub |
||||
describe('#createPendingNonceMiddleware', function () { |
||||
const getPendingNonce = async () => '0x2' |
||||
const address = '0xF231D46dD78806E1DD93442cf33C7671f8538748' |
||||
const pendingNonceMiddleware = createPendingNonceMiddleware({ getPendingNonce }) |
||||
|
||||
it('should call next if not a eth_getTransactionCount request', (done) => { |
||||
const req = {method: 'eth_getBlockByNumber'} |
||||
const res = {} |
||||
pendingNonceMiddleware(req, res, () => done()) |
||||
}) |
||||
it('should call next if not a "pending" block request', (done) => { |
||||
const req = { method: 'eth_getTransactionCount', params: [address] } |
||||
const res = {} |
||||
pendingNonceMiddleware(req, res, () => done()) |
||||
}) |
||||
it('should fill the result with a the "pending" nonce', (done) => { |
||||
const req = { method: 'eth_getTransactionCount', params: [address, 'pending'] } |
||||
const res = {} |
||||
pendingNonceMiddleware(req, res, () => { done(new Error('should not have called next')) }, () => { |
||||
assert(res.result === '0x2') |
||||
done() |
||||
}) |
||||
}) |
||||
}) |
||||
|
||||
describe('#createPendingTxMiddleware', function () { |
||||
let returnUndefined = true |
||||
const getPendingTransactionByHash = () => returnUndefined ? undefined : txMetaStub |
||||
const address = '0xF231D46dD78806E1DD93442cf33C7671f8538748' |
||||
const pendingTxMiddleware = createPendingTxMiddleware({ getPendingTransactionByHash }) |
||||
const spec = { |
||||
'blockHash': null, |
||||
'blockNumber': null, |
||||
'from': '0xf231d46dd78806e1dd93442cf33c7671f8538748', |
||||
'gas': '0x5208', |
||||
'gasPrice': '0x1e8480', |
||||
'hash': '0x2cc5a25744486f7383edebbf32003e5a66e18135799593d6b5cdd2bb43674f09', |
||||
'input': '0x', |
||||
'nonce': '0x4', |
||||
'to': '0xf231d46dd78806e1dd93442cf33c7671f8538748', |
||||
'transactionIndex': null, |
||||
'value': '0x0', |
||||
'v': '0x2c', |
||||
'r': '0x5f973e540f2d3c2f06d3725a626b75247593cb36477187ae07ecfe0a4db3cf57', |
||||
's': '0x0259b52ee8c58baaa385fb05c3f96116e58de89bcc165cb3bfdfc708672fed8a', |
||||
} |
||||
it('should call next if not a eth_getTransactionByHash request', (done) => { |
||||
const req = {method: 'eth_getBlockByNumber'} |
||||
const res = {} |
||||
pendingTxMiddleware(req, res, () => done()) |
||||
}) |
||||
|
||||
it('should call next if no pending txMeta is in history', (done) => { |
||||
const req = { method: 'eth_getTransactionByHash', params: [address] } |
||||
const res = {} |
||||
pendingTxMiddleware(req, res, () => done()) |
||||
}) |
||||
|
||||
it('should fill the result with a the "pending" tx the result should match the rpc spec', (done) => { |
||||
returnUndefined = false |
||||
const req = { method: 'eth_getTransactionByHash', params: [address, 'pending'] } |
||||
const res = {} |
||||
pendingTxMiddleware(req, res, () => { done(new Error('should not have called next')) }, () => { |
||||
/* |
||||
// uncomment this section for debugging help with non matching keys
|
||||
const coppy = {...res.result} |
||||
Object.keys(spec).forEach((key) => { |
||||
console.log(coppy[key], '===', spec[key], coppy[key] === spec[key], key) |
||||
delete coppy[key] |
||||
}) |
||||
console.log(coppy) |
||||
*/ |
||||
assert.deepStrictEqual(res.result, spec, new Error('result does not match the spec object')) |
||||
done() |
||||
}) |
||||
}) |
||||
|
||||
}) |
@ -0,0 +1,225 @@ |
||||
/* |
||||
this file is for all my big stubs because i don't want to |
||||
to mingle with my tests |
||||
*/ |
||||
|
||||
module.exports = {} |
||||
|
||||
// for pending middlewares test
|
||||
module.exports.txMetaStub = { |
||||
'estimatedGas': '0x5208', |
||||
'firstRetryBlockNumber': '0x51a402', |
||||
'gasLimitSpecified': true, |
||||
'gasPriceSpecified': true, |
||||
'hash': '0x2cc5a25744486f7383edebbf32003e5a66e18135799593d6b5cdd2bb43674f09', |
||||
'history': [ |
||||
{ |
||||
'id': 405984854664302, |
||||
'loadingDefaults': true, |
||||
'metamaskNetworkId': '4', |
||||
'status': 'unapproved', |
||||
'time': 1572395156620, |
||||
'transactionCategory': 'sentEther', |
||||
'txParams': { |
||||
'from': '0xf231d46dd78806e1dd93442cf33c7671f8538748', |
||||
'gas': '0x5208', |
||||
'gasPrice': '0x1e8480', |
||||
'to': '0xf231d46dd78806e1dd93442cf33c7671f8538748', |
||||
'value': '0x0', |
||||
}, |
||||
'type': 'standard', |
||||
}, |
||||
[ |
||||
{ |
||||
'op': 'replace', |
||||
'path': '/loadingDefaults', |
||||
'timestamp': 1572395156645, |
||||
'value': false, |
||||
}, |
||||
{ |
||||
'op': 'add', |
||||
'path': '/gasPriceSpecified', |
||||
'value': true, |
||||
}, |
||||
{ |
||||
'op': 'add', |
||||
'path': '/gasLimitSpecified', |
||||
'value': true, |
||||
}, |
||||
{ |
||||
'op': 'add', |
||||
'path': '/estimatedGas', |
||||
'value': '0x5208', |
||||
}, |
||||
], |
||||
[ |
||||
{ |
||||
'note': '#newUnapprovedTransaction - adding the origin', |
||||
'op': 'add', |
||||
'path': '/origin', |
||||
'timestamp': 1572395156645, |
||||
'value': 'MetaMask', |
||||
}, |
||||
], |
||||
[], |
||||
[ |
||||
{ |
||||
'note': 'txStateManager: setting status to approved', |
||||
'op': 'replace', |
||||
'path': '/status', |
||||
'timestamp': 1572395158240, |
||||
'value': 'approved', |
||||
}, |
||||
], |
||||
[ |
||||
{ |
||||
'note': 'transactions#approveTransaction', |
||||
'op': 'add', |
||||
'path': '/txParams/nonce', |
||||
'timestamp': 1572395158261, |
||||
'value': '0x4', |
||||
}, |
||||
{ |
||||
'op': 'add', |
||||
'path': '/nonceDetails', |
||||
'value': { |
||||
'local': { |
||||
'details': { |
||||
'highest': 4, |
||||
'startPoint': 4, |
||||
}, |
||||
'name': 'local', |
||||
'nonce': 4, |
||||
}, |
||||
'network': { |
||||
'details': { |
||||
'baseCount': 4, |
||||
'blockNumber': '0x51a401', |
||||
}, |
||||
'name': 'network', |
||||
'nonce': 4, |
||||
}, |
||||
'params': { |
||||
'highestLocallyConfirmed': 0, |
||||
'highestSuggested': 4, |
||||
'nextNetworkNonce': 4, |
||||
}, |
||||
}, |
||||
}, |
||||
], |
||||
[ |
||||
{ |
||||
'note': 'transactions#signTransaction: add r, s, v values', |
||||
'op': 'add', |
||||
'path': '/r', |
||||
'timestamp': 1572395158280, |
||||
'value': '0x5f973e540f2d3c2f06d3725a626b75247593cb36477187ae07ecfe0a4db3cf57', |
||||
}, |
||||
{ |
||||
'op': 'add', |
||||
'path': '/s', |
||||
'value': '0x0259b52ee8c58baaa385fb05c3f96116e58de89bcc165cb3bfdfc708672fed8a', |
||||
}, |
||||
{ |
||||
'op': 'add', |
||||
'path': '/v', |
||||
'value': '0x2c', |
||||
}, |
||||
], |
||||
[ |
||||
{ |
||||
'note': 'transactions#publishTransaction', |
||||
'op': 'replace', |
||||
'path': '/status', |
||||
'timestamp': 1572395158281, |
||||
'value': 'signed', |
||||
}, |
||||
{ |
||||
'op': 'add', |
||||
'path': '/rawTx', |
||||
'value': '0xf86204831e848082520894f231d46dd78806e1dd93442cf33c7671f853874880802ca05f973e540f2d3c2f06d3725a626b75247593cb36477187ae07ecfe0a4db3cf57a00259b52ee8c58baaa385fb05c3f96116e58de89bcc165cb3bfdfc708672fed8a', |
||||
}, |
||||
], |
||||
[], |
||||
[ |
||||
{ |
||||
'note': 'transactions#setTxHash', |
||||
'op': 'add', |
||||
'path': '/hash', |
||||
'timestamp': 1572395158570, |
||||
'value': '0x2cc5a25744486f7383edebbf32003e5a66e18135799593d6b5cdd2bb43674f09', |
||||
}, |
||||
], |
||||
[ |
||||
{ |
||||
'note': 'txStateManager - add submitted time stamp', |
||||
'op': 'add', |
||||
'path': '/submittedTime', |
||||
'timestamp': 1572395158571, |
||||
'value': 1572395158570, |
||||
}, |
||||
], |
||||
[ |
||||
{ |
||||
'note': 'txStateManager: setting status to submitted', |
||||
'op': 'replace', |
||||
'path': '/status', |
||||
'timestamp': 1572395158576, |
||||
'value': 'submitted', |
||||
}, |
||||
], |
||||
[ |
||||
{ |
||||
'note': 'transactions/pending-tx-tracker#event: tx:block-update', |
||||
'op': 'add', |
||||
'path': '/firstRetryBlockNumber', |
||||
'timestamp': 1572395168972, |
||||
'value': '0x51a402', |
||||
}, |
||||
], |
||||
], |
||||
'id': 405984854664302, |
||||
'loadingDefaults': false, |
||||
'metamaskNetworkId': '4', |
||||
'nonceDetails': { |
||||
'local': { |
||||
'details': { |
||||
'highest': 4, |
||||
'startPoint': 4, |
||||
}, |
||||
'name': 'local', |
||||
'nonce': 4, |
||||
}, |
||||
'network': { |
||||
'details': { |
||||
'baseCount': 4, |
||||
'blockNumber': '0x51a401', |
||||
}, |
||||
'name': 'network', |
||||
'nonce': 4, |
||||
}, |
||||
'params': { |
||||
'highestLocallyConfirmed': 0, |
||||
'highestSuggested': 4, |
||||
'nextNetworkNonce': 4, |
||||
}, |
||||
}, |
||||
'origin': 'MetaMask', |
||||
'r': '0x5f973e540f2d3c2f06d3725a626b75247593cb36477187ae07ecfe0a4db3cf57', |
||||
'rawTx': '0xf86204831e848082520894f231d46dd78806e1dd93442cf33c7671f853874880802ca05f973e540f2d3c2f06d3725a626b75247593cb36477187ae07ecfe0a4db3cf57a00259b52ee8c58baaa385fb05c3f96116e58de89bcc165cb3bfdfc708672fed8a', |
||||
's': '0x0259b52ee8c58baaa385fb05c3f96116e58de89bcc165cb3bfdfc708672fed8a', |
||||
'status': 'submitted', |
||||
'submittedTime': 1572395158570, |
||||
'time': 1572395156620, |
||||
'transactionCategory': 'sentEther', |
||||
'txParams': { |
||||
'from': '0xf231d46dd78806e1dd93442cf33c7671f8538748', |
||||
'gas': '0x5208', |
||||
'gasPrice': '0x1e8480', |
||||
'nonce': '0x4', |
||||
'to': '0xf231d46dd78806e1dd93442cf33c7671f8538748', |
||||
'value': '0x0', |
||||
}, |
||||
'type': 'standard', |
||||
'v': '0x2c', |
||||
} |
@ -0,0 +1,170 @@ |
||||
import React, { PureComponent } from 'react' |
||||
import PropTypes from 'prop-types' |
||||
import Modal from '../../modal' |
||||
import Identicon from '../../../ui/identicon' |
||||
import TextField from '../../../ui/text-field' |
||||
import classnames from 'classnames' |
||||
|
||||
export default class EditApprovalPermission extends PureComponent { |
||||
static propTypes = { |
||||
hideModal: PropTypes.func.isRequired, |
||||
selectedIdentity: PropTypes.object, |
||||
tokenAmount: PropTypes.string, |
||||
customTokenAmount: PropTypes.string, |
||||
tokenSymbol: PropTypes.string, |
||||
tokenBalance: PropTypes.string, |
||||
setCustomAmount: PropTypes.func, |
||||
origin: PropTypes.string, |
||||
} |
||||
|
||||
static contextTypes = { |
||||
t: PropTypes.func, |
||||
} |
||||
|
||||
state = { |
||||
customSpendLimit: this.props.customTokenAmount, |
||||
selectedOptionIsUnlimited: !this.props.customTokenAmount, |
||||
} |
||||
|
||||
renderModalContent () { |
||||
const { t } = this.context |
||||
const { |
||||
hideModal, |
||||
selectedIdentity, |
||||
tokenAmount, |
||||
tokenSymbol, |
||||
tokenBalance, |
||||
customTokenAmount, |
||||
origin, |
||||
} = this.props |
||||
const { name, address } = selectedIdentity || {} |
||||
const { selectedOptionIsUnlimited } = this.state |
||||
|
||||
return ( |
||||
<div className="edit-approval-permission"> |
||||
<div className="edit-approval-permission__header"> |
||||
<div className="edit-approval-permission__title"> |
||||
{ t('editPermission') } |
||||
</div> |
||||
<div |
||||
className="edit-approval-permission__header__close" |
||||
onClick={() => hideModal()} |
||||
/> |
||||
</div> |
||||
<div className="edit-approval-permission__account-info"> |
||||
<div className="edit-approval-permission__account-info__account"> |
||||
<Identicon |
||||
address={address} |
||||
diameter={32} |
||||
/> |
||||
<div className="edit-approval-permission__account-info__name">{ name }</div> |
||||
<div>{ t('balance') }</div> |
||||
</div> |
||||
<div className="edit-approval-permission__account-info__balance"> |
||||
{`${tokenBalance} ${tokenSymbol}`} |
||||
</div> |
||||
</div> |
||||
<div className="edit-approval-permission__edit-section"> |
||||
<div className="edit-approval-permission__edit-section__title"> |
||||
{ t('spendLimitPermission') } |
||||
</div> |
||||
<div className="edit-approval-permission__edit-section__description"> |
||||
{ t('allowWithdrawAndSpend', [origin]) } |
||||
</div> |
||||
<div className="edit-approval-permission__edit-section__option"> |
||||
<div |
||||
className="edit-approval-permission__edit-section__radio-button" |
||||
onClick={() => this.setState({ selectedOptionIsUnlimited: true })} |
||||
> |
||||
<div className={classnames({ |
||||
'edit-approval-permission__edit-section__radio-button-outline': !selectedOptionIsUnlimited, |
||||
'edit-approval-permission__edit-section__radio-button-outline--selected': selectedOptionIsUnlimited, |
||||
})} /> |
||||
<div className="edit-approval-permission__edit-section__radio-button-fill" /> |
||||
{ selectedOptionIsUnlimited && <div className="edit-approval-permission__edit-section__radio-button-dot" />} |
||||
</div> |
||||
<div className="edit-approval-permission__edit-section__option-text"> |
||||
<div className={classnames({ |
||||
'edit-approval-permission__edit-section__option-label': !selectedOptionIsUnlimited, |
||||
'edit-approval-permission__edit-section__option-label--selected': selectedOptionIsUnlimited, |
||||
})}> |
||||
{ |
||||
tokenAmount < tokenBalance |
||||
? t('proposedApprovalLimit') |
||||
: t('unlimited') |
||||
} |
||||
</div> |
||||
<div className="edit-approval-permission__edit-section__option-description" > |
||||
{ t('spendLimitRequestedBy', [origin]) } |
||||
</div> |
||||
<div className="edit-approval-permission__edit-section__option-value" > |
||||
{`${tokenAmount} ${tokenSymbol}`} |
||||
</div> |
||||
</div> |
||||
</div> |
||||
<div className="edit-approval-permission__edit-section__option"> |
||||
<div |
||||
className="edit-approval-permission__edit-section__radio-button" |
||||
onClick={() => this.setState({ selectedOptionIsUnlimited: false })} |
||||
> |
||||
<div className={classnames({ |
||||
'edit-approval-permission__edit-section__radio-button-outline': selectedOptionIsUnlimited, |
||||
'edit-approval-permission__edit-section__radio-button-outline--selected': !selectedOptionIsUnlimited, |
||||
})} /> |
||||
<div className="edit-approval-permission__edit-section__radio-button-fill" /> |
||||
{ !selectedOptionIsUnlimited && <div className="edit-approval-permission__edit-section__radio-button-dot" />} |
||||
</div> |
||||
<div className="edit-approval-permission__edit-section__option-text"> |
||||
<div className={classnames({ |
||||
'edit-approval-permission__edit-section__option-label': selectedOptionIsUnlimited, |
||||
'edit-approval-permission__edit-section__option-label--selected': !selectedOptionIsUnlimited, |
||||
})}> |
||||
{ t('customSpendLimit') } |
||||
</div> |
||||
<div className="edit-approval-permission__edit-section__option-description" > |
||||
{ t('enterMaxSpendLimit') } |
||||
</div> |
||||
<div className="edit-approval-permission__edit-section__option-input" > |
||||
<TextField |
||||
type="number" |
||||
min="0" |
||||
placeholder={ `${customTokenAmount || tokenAmount} ${tokenSymbol}` } |
||||
onChange={(event) => { |
||||
this.setState({ customSpendLimit: event.target.value }) |
||||
if (selectedOptionIsUnlimited) { |
||||
this.setState({ selectedOptionIsUnlimited: false }) |
||||
} |
||||
}} |
||||
fullWidth |
||||
margin="dense" |
||||
value={ this.state.customSpendLimit } |
||||
/> |
||||
</div> |
||||
</div> |
||||
</div> |
||||
</div> |
||||
</div> |
||||
) |
||||
} |
||||
|
||||
render () { |
||||
const { t } = this.context |
||||
const { setCustomAmount, hideModal, customTokenAmount } = this.props |
||||
const { selectedOptionIsUnlimited, customSpendLimit } = this.state |
||||
return ( |
||||
<Modal |
||||
onSubmit={() => { |
||||
setCustomAmount(!selectedOptionIsUnlimited ? customSpendLimit : '') |
||||
hideModal() |
||||
}} |
||||
submitText={t('save')} |
||||
submitType="primary" |
||||
contentClass="edit-approval-permission-modal-content" |
||||
containerClass="edit-approval-permission-modal-container" |
||||
submitDisabled={ (customSpendLimit === customTokenAmount) && !selectedOptionIsUnlimited } |
||||
> |
||||
{ this.renderModalContent() } |
||||
</Modal> |
||||
) |
||||
} |
||||
} |
@ -0,0 +1,18 @@ |
||||
import { connect } from 'react-redux' |
||||
import { compose } from 'recompose' |
||||
import withModalProps from '../../../../helpers/higher-order-components/with-modal-props' |
||||
import EditApprovalPermission from './edit-approval-permission.component' |
||||
import { getSelectedIdentity } from '../../../../selectors/selectors' |
||||
|
||||
const mapStateToProps = (state) => { |
||||
const modalStateProps = state.appState.modal.modalState.props || {} |
||||
return { |
||||
selectedIdentity: getSelectedIdentity(state), |
||||
...modalStateProps, |
||||
} |
||||
} |
||||
|
||||
export default compose( |
||||
withModalProps, |
||||
connect(mapStateToProps) |
||||
)(EditApprovalPermission) |
@ -0,0 +1 @@ |
||||
export { default } from './edit-approval-permission.container' |
@ -0,0 +1,167 @@ |
||||
.edit-approval-permission { |
||||
width: 100%; |
||||
|
||||
&__header, |
||||
&__account-info { |
||||
display: flex; |
||||
justify-content: center; |
||||
align-items: center; |
||||
position: relative; |
||||
border-bottom: 1px solid #d2d8dd; |
||||
} |
||||
|
||||
&__header { |
||||
padding: 24px; |
||||
|
||||
&__close { |
||||
position: absolute; |
||||
right: 24px; |
||||
background-image: url("/images/close-gray.svg"); |
||||
width: .75rem; |
||||
height: .75rem; |
||||
cursor: pointer; |
||||
} |
||||
} |
||||
|
||||
&__title { |
||||
font-weight: bold; |
||||
font-size: 18px; |
||||
line-height: 25px; |
||||
} |
||||
|
||||
&__account-info { |
||||
justify-content: space-between; |
||||
padding: 8px 24px; |
||||
|
||||
&__account, |
||||
&__balance { |
||||
font-weight: normal; |
||||
font-size: 14px; |
||||
color: #24292E; |
||||
} |
||||
|
||||
&__account { |
||||
display: flex; |
||||
align-items: center; |
||||
} |
||||
|
||||
&__name { |
||||
margin-left: 8px; |
||||
margin-right: 8px; |
||||
} |
||||
|
||||
&__balance { |
||||
color: #6A737D; |
||||
} |
||||
} |
||||
|
||||
&__edit-section { |
||||
padding: 24px; |
||||
|
||||
&__title { |
||||
font-weight: bold; |
||||
font-size: 14px; |
||||
line-height: 20px; |
||||
color: #24292E; |
||||
} |
||||
|
||||
&__description { |
||||
font-weight: normal; |
||||
font-size: 12px; |
||||
line-height: 17px; |
||||
color: #6A737D; |
||||
margin-top: 8px; |
||||
} |
||||
|
||||
&__option { |
||||
display: flex; |
||||
align-items: flex-start; |
||||
margin-top: 20px; |
||||
} |
||||
|
||||
&__radio-button { |
||||
width: 18px; |
||||
} |
||||
|
||||
&__option-text { |
||||
display: flex; |
||||
flex-direction: column; |
||||
} |
||||
|
||||
&__option-label, |
||||
&__option-label--selected { |
||||
font-weight: normal; |
||||
font-size: 14px; |
||||
line-height: 20px; |
||||
color: #474B4D; |
||||
} |
||||
|
||||
&__option-label--selected { |
||||
color: #037DD6; |
||||
} |
||||
|
||||
&__option-description { |
||||
font-weight: normal; |
||||
font-size: 12px; |
||||
line-height: 17px; |
||||
color: #6A737D; |
||||
margin-top: 8px; |
||||
margin-bottom: 6px; |
||||
} |
||||
|
||||
&__option-value { |
||||
font-weight: normal; |
||||
font-size: 18px; |
||||
line-height: 25px; |
||||
color: #24292E; |
||||
} |
||||
|
||||
&__radio-button { |
||||
position: relative; |
||||
width: 18px; |
||||
height: 18px; |
||||
display: flex; |
||||
justify-content: center; |
||||
align-items: center; |
||||
margin-right: 4px; |
||||
} |
||||
|
||||
&__radio-button-outline, |
||||
&__radio-button-outline--selected { |
||||
width: 18px; |
||||
height: 18px; |
||||
background: #DADCDD; |
||||
border-radius: 9px; |
||||
position: absolute; |
||||
} |
||||
|
||||
&__radio-button-outline--selected { |
||||
background: #037DD6; |
||||
} |
||||
|
||||
&__radio-button-fill { |
||||
width: 14px; |
||||
height: 14px; |
||||
background: white; |
||||
border-radius: 7px; |
||||
position: absolute; |
||||
} |
||||
|
||||
&__radio-button-dot { |
||||
width: 8px; |
||||
height: 8px; |
||||
background: #037DD6; |
||||
border-radius: 4px; |
||||
position: absolute; |
||||
} |
||||
} |
||||
} |
||||
|
||||
.edit-approval-permission-modal-content { |
||||
padding: 0px; |
||||
} |
||||
|
||||
.edit-approval-permission-modal-container { |
||||
max-height: 550px; |
||||
width: 100%; |
||||
} |
@ -0,0 +1 @@ |
||||
export { default } from './signature-request.container' |
@ -0,0 +1,96 @@ |
||||
@import 'signature-request-footer/index'; |
||||
@import 'signature-request-header/index'; |
||||
@import 'signature-request-message/index'; |
||||
|
||||
.signature-request { |
||||
display: flex; |
||||
flex: 1 1 auto; |
||||
flex-direction: column; |
||||
min-width: 0; |
||||
|
||||
@media screen and (min-width: 576px) { |
||||
flex: initial; |
||||
} |
||||
} |
||||
|
||||
.signature-request-header { |
||||
flex: 1; |
||||
|
||||
.network-display__container { |
||||
padding: 0; |
||||
justify-content: flex-end; |
||||
} |
||||
|
||||
.network-display__name { |
||||
font-size: 12px; |
||||
white-space: nowrap; |
||||
font-weight: 500; |
||||
} |
||||
} |
||||
|
||||
.signature-request-content { |
||||
flex: 1 40%; |
||||
margin-top: 1rem; |
||||
display: flex; |
||||
align-items: center; |
||||
flex-direction: column; |
||||
margin-bottom: 25px; |
||||
min-height: min-content; |
||||
|
||||
&__title { |
||||
font-family: Roboto; |
||||
font-style: normal; |
||||
font-weight: 500; |
||||
font-size: 18px; |
||||
} |
||||
|
||||
&__identicon-container { |
||||
padding: 1rem; |
||||
flex: 1; |
||||
position: relative; |
||||
width: 100%; |
||||
display: flex; |
||||
justify-content: center; |
||||
align-items: center; |
||||
} |
||||
|
||||
&__identicon-border { |
||||
height: 75px; |
||||
width: 75px; |
||||
border-radius: 50%; |
||||
border: 1px solid white; |
||||
position: absolute; |
||||
box-shadow: 0 2px 2px 0.5px rgba(0, 0, 0, 0.19); |
||||
} |
||||
|
||||
&__identicon-initial { |
||||
position: absolute; |
||||
font-family: Roboto; |
||||
font-style: normal; |
||||
font-weight: 500; |
||||
font-size: 60px; |
||||
color: white; |
||||
z-index: 1; |
||||
text-shadow: 0px 4px 6px rgba(0, 0, 0, 0.422); |
||||
} |
||||
|
||||
&__info { |
||||
font-size: 12px; |
||||
} |
||||
|
||||
&__info--bolded { |
||||
font-size: 16px; |
||||
font-weight: 500; |
||||
} |
||||
|
||||
p { |
||||
color: #999999; |
||||
font-size: 0.8rem; |
||||
} |
||||
|
||||
.identicon {} |
||||
} |
||||
|
||||
.signature-request-footer { |
||||
flex: 1 1 auto; |
||||
} |
@ -0,0 +1 @@ |
||||
export { default } from './signature-request-footer.component' |
@ -0,0 +1,18 @@ |
||||
.signature-request-footer { |
||||
display: flex; |
||||
border-top: 1px solid #d2d8dd; |
||||
|
||||
button { |
||||
text-transform: uppercase; |
||||
flex: 1; |
||||
margin: 1rem 0.5rem; |
||||
border-radius: 3px; |
||||
} |
||||
|
||||
button:first-child() { |
||||
margin-left: 1rem; |
||||
} |
||||
button:last-child() { |
||||
margin-right: 1rem; |
||||
} |
||||
} |
@ -0,0 +1,24 @@ |
||||
import React, { PureComponent } from 'react' |
||||
import PropTypes from 'prop-types' |
||||
import Button from '../../../ui/button' |
||||
|
||||
export default class SignatureRequestFooter extends PureComponent { |
||||
static propTypes = { |
||||
cancelAction: PropTypes.func.isRequired, |
||||
signAction: PropTypes.func.isRequired, |
||||
} |
||||
|
||||
static contextTypes = { |
||||
t: PropTypes.func, |
||||
} |
||||
|
||||
render () { |
||||
const { cancelAction, signAction } = this.props |
||||
return ( |
||||
<div className="signature-request-footer"> |
||||
<Button onClick={cancelAction} type="default" large>{this.context.t('cancel')}</Button> |
||||
<Button onClick={signAction} type="primary" large>{this.context.t('sign')}</Button> |
||||
</div> |
||||
) |
||||
} |
||||
} |
@ -0,0 +1 @@ |
||||
export { default } from './signature-request-header.component' |
@ -0,0 +1,25 @@ |
||||
.signature-request-header { |
||||
display: flex; |
||||
padding: 1rem; |
||||
border-bottom: 1px solid $geyser; |
||||
justify-content: space-between; |
||||
font-size: .75rem; |
||||
|
||||
&--account, &--network { |
||||
flex: 1; |
||||
} |
||||
|
||||
&--account { |
||||
display: flex; |
||||
align-items: center; |
||||
|
||||
.account-list-item__account-name { |
||||
font-size: 12px; |
||||
font-weight: 500; |
||||
} |
||||
|
||||
.account-list-item__top-row { |
||||
margin: 0px; |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,29 @@ |
||||
import React, { PureComponent } from 'react' |
||||
import PropTypes from 'prop-types' |
||||
import AccountListItem from '../../../../pages/send/account-list-item/account-list-item.component' |
||||
import NetworkDisplay from '../../network-display' |
||||
|
||||
export default class SignatureRequestHeader extends PureComponent { |
||||
static propTypes = { |
||||
selectedAccount: PropTypes.object.isRequired, |
||||
} |
||||
|
||||
render () { |
||||
const { selectedAccount } = this.props |
||||
|
||||
return ( |
||||
<div className="signature-request-header"> |
||||
<div className="signature-request-header--account"> |
||||
{selectedAccount && <AccountListItem |
||||
displayBalance={false} |
||||
account={selectedAccount} |
||||
/>} |
||||
{name} |
||||
</div> |
||||
<div className="signature-request-header--network"> |
||||
<NetworkDisplay colored={false} /> |
||||
</div> |
||||
</div> |
||||
) |
||||
} |
||||
} |
@ -0,0 +1 @@ |
||||
export { default } from './signature-request-message.component' |
@ -0,0 +1,67 @@ |
||||
.signature-request-message { |
||||
flex: 1 60%; |
||||
display: flex; |
||||
flex-direction: column; |
||||
|
||||
&__title { |
||||
font-weight: 500; |
||||
font-size: 14px; |
||||
color: #636778; |
||||
margin-left: 12px; |
||||
} |
||||
|
||||
h2 { |
||||
flex: 1 1 0; |
||||
text-align: left; |
||||
font-size: 0.8rem; |
||||
border-bottom: 1px solid #d2d8dd; |
||||
padding: 0.5rem; |
||||
margin: 0; |
||||
color: #ccc; |
||||
} |
||||
|
||||
&--root { |
||||
flex: 1 100%; |
||||
background-color: #f8f9fb; |
||||
padding-bottom: 0.5rem; |
||||
overflow: auto; |
||||
padding-left: 12px; |
||||
padding-right: 12px; |
||||
width: 360px; |
||||
font-family: monospace; |
||||
|
||||
@media screen and (min-width: 576px) { |
||||
width: auto; |
||||
} |
||||
} |
||||
|
||||
&__type-title { |
||||
font-family: monospace; |
||||
font-style: normal; |
||||
font-weight: normal; |
||||
font-size: 14px; |
||||
margin-left: 12px; |
||||
margin-top: 6px; |
||||
margin-bottom: 10px; |
||||
} |
||||
|
||||
&--node, &--node-leaf { |
||||
padding-left: 0.8rem; |
||||
|
||||
&-label { |
||||
color: #5B5D67; |
||||
} |
||||
|
||||
&-value { |
||||
color: black; |
||||
margin-left: 0.5rem; |
||||
text-overflow: ellipsis; |
||||
white-space: nowrap; |
||||
overflow: hidden; |
||||
} |
||||
} |
||||
|
||||
&--node-leaf { |
||||
display: flex; |
||||
} |
||||
} |
@ -0,0 +1,50 @@ |
||||
import React, { PureComponent } from 'react' |
||||
import PropTypes from 'prop-types' |
||||
import classnames from 'classnames' |
||||
|
||||
export default class SignatureRequestMessage extends PureComponent { |
||||
static propTypes = { |
||||
data: PropTypes.object.isRequired, |
||||
} |
||||
|
||||
static contextTypes = { |
||||
t: PropTypes.func, |
||||
} |
||||
|
||||
renderNode (data) { |
||||
return ( |
||||
<div className="signature-request-message--node"> |
||||
{Object.entries(data).map(([ label, value ], i) => ( |
||||
<div |
||||
className={classnames('signature-request-message--node', { |
||||
'signature-request-message--node-leaf': typeof value !== 'object' || value === null, |
||||
})} |
||||
key={i} |
||||
> |
||||
<span className="signature-request-message--node-label">{label}: </span> |
||||
{ |
||||
typeof value === 'object' && value !== null ? |
||||
this.renderNode(value) |
||||
: <span className="signature-request-message--node-value">{value}</span> |
||||
} |
||||
</div> |
||||
))} |
||||
</div> |
||||
) |
||||
} |
||||
|
||||
|
||||
render () { |
||||
const { data } = this.props |
||||
|
||||
return ( |
||||
<div className="signature-request-message"> |
||||
<div className="signature-request-message__title">{this.context.t('signatureRequest1')}</div> |
||||
<div className="signature-request-message--root"> |
||||
<div className="signature-request-message__type-title">{this.context.t('signatureRequest1')}</div> |
||||
{this.renderNode(data)} |
||||
</div> |
||||
</div> |
||||
) |
||||
} |
||||
} |
@ -0,0 +1,81 @@ |
||||
import React, { PureComponent } from 'react' |
||||
import PropTypes from 'prop-types' |
||||
import Header from './signature-request-header' |
||||
import Footer from './signature-request-footer' |
||||
import Message from './signature-request-message' |
||||
import { ENVIRONMENT_TYPE_NOTIFICATION } from './signature-request.constants' |
||||
import { getEnvironmentType } from '../../../../../app/scripts/lib/util' |
||||
import Identicon from '../../ui/identicon' |
||||
|
||||
export default class SignatureRequest extends PureComponent { |
||||
static propTypes = { |
||||
txData: PropTypes.object.isRequired, |
||||
selectedAccount: PropTypes.shape({ |
||||
address: PropTypes.string, |
||||
balance: PropTypes.string, |
||||
name: PropTypes.string, |
||||
}).isRequired, |
||||
|
||||
clearConfirmTransaction: PropTypes.func.isRequired, |
||||
cancel: PropTypes.func.isRequired, |
||||
sign: PropTypes.func.isRequired, |
||||
} |
||||
|
||||
static contextTypes = { |
||||
t: PropTypes.func, |
||||
} |
||||
|
||||
componentDidMount () { |
||||
const { clearConfirmTransaction, cancel } = this.props |
||||
const { metricsEvent } = this.context |
||||
if (getEnvironmentType(window.location.href) === ENVIRONMENT_TYPE_NOTIFICATION) { |
||||
window.addEventListener('beforeunload', (event) => { |
||||
metricsEvent({ |
||||
eventOpts: { |
||||
category: 'Transactions', |
||||
action: 'Sign Request', |
||||
name: 'Cancel Sig Request Via Notification Close', |
||||
}, |
||||
}) |
||||
clearConfirmTransaction() |
||||
cancel(event) |
||||
}) |
||||
} |
||||
} |
||||
|
||||
formatWallet (wallet) { |
||||
return `${wallet.slice(0, 8)}...${wallet.slice(wallet.length - 8, wallet.length)}` |
||||
} |
||||
|
||||
render () { |
||||
const { |
||||
selectedAccount, |
||||
txData: { msgParams: { data, origin, from: senderWallet }}, |
||||
cancel, |
||||
sign, |
||||
} = this.props |
||||
const { message } = JSON.parse(data) |
||||
|
||||
return ( |
||||
<div className="signature-request page-container"> |
||||
<Header selectedAccount={selectedAccount} /> |
||||
<div className="signature-request-content"> |
||||
<div className="signature-request-content__title">{this.context.t('sigRequest')}</div> |
||||
<div className="signature-request-content__identicon-container"> |
||||
<div className="signature-request-content__identicon-initial" >{ message.from.name && message.from.name[0] }</div> |
||||
<div className="signature-request-content__identicon-border" /> |
||||
<Identicon |
||||
address={message.from.wallet} |
||||
diameter={70} |
||||
/> |
||||
</div> |
||||
<div className="signature-request-content__info--bolded">{message.from.name}</div> |
||||
<div className="signature-request-content__info">{origin}</div> |
||||
<div className="signature-request-content__info">{this.formatWallet(senderWallet)}</div> |
||||
</div> |
||||
<Message data={message} /> |
||||
<Footer cancelAction={cancel} signAction={sign} /> |
||||
</div> |
||||
) |
||||
} |
||||
} |
@ -0,0 +1,3 @@ |
||||
import { ENVIRONMENT_TYPE_NOTIFICATION } from '../../../../../app/scripts/lib/enums' |
||||
|
||||
export { ENVIRONMENT_TYPE_NOTIFICATION } |
@ -0,0 +1,72 @@ |
||||
import { connect } from 'react-redux' |
||||
import { withRouter } from 'react-router-dom' |
||||
import { compose } from 'recompose' |
||||
import SignatureRequest from './signature-request.component' |
||||
import { goHome } from '../../../store/actions' |
||||
import { clearConfirmTransaction } from '../../../ducks/confirm-transaction/confirm-transaction.duck' |
||||
import { |
||||
getSelectedAccount, |
||||
getCurrentAccountWithSendEtherInfo, |
||||
getSelectedAddress, |
||||
accountsWithSendEtherInfoSelector, |
||||
conversionRateSelector, |
||||
} from '../../../selectors/selectors.js' |
||||
|
||||
function mapStateToProps (state) { |
||||
return { |
||||
balance: getSelectedAccount(state).balance, |
||||
selectedAccount: getCurrentAccountWithSendEtherInfo(state), |
||||
selectedAddress: getSelectedAddress(state), |
||||
accounts: accountsWithSendEtherInfoSelector(state), |
||||
conversionRate: conversionRateSelector(state), |
||||
} |
||||
} |
||||
|
||||
function mapDispatchToProps (dispatch) { |
||||
return { |
||||
goHome: () => dispatch(goHome()), |
||||
clearConfirmTransaction: () => dispatch(clearConfirmTransaction()), |
||||
} |
||||
} |
||||
|
||||
function mergeProps (stateProps, dispatchProps, ownProps) { |
||||
const { |
||||
signPersonalMessage, |
||||
signTypedMessage, |
||||
cancelPersonalMessage, |
||||
cancelTypedMessage, |
||||
signMessage, |
||||
cancelMessage, |
||||
txData, |
||||
} = ownProps |
||||
|
||||
const { type } = txData |
||||
|
||||
let cancel |
||||
let sign |
||||
|
||||
if (type === 'personal_sign') { |
||||
cancel = cancelPersonalMessage |
||||
sign = signPersonalMessage |
||||
} else if (type === 'eth_signTypedData') { |
||||
cancel = cancelTypedMessage |
||||
sign = signTypedMessage |
||||
} else if (type === 'eth_sign') { |
||||
cancel = cancelMessage |
||||
sign = signMessage |
||||
} |
||||
|
||||
return { |
||||
...stateProps, |
||||
...dispatchProps, |
||||
...ownProps, |
||||
txData, |
||||
cancel, |
||||
sign, |
||||
} |
||||
} |
||||
|
||||
export default compose( |
||||
withRouter, |
||||
connect(mapStateToProps, mapDispatchToProps, mergeProps) |
||||
)(SignatureRequest) |
@ -0,0 +1,25 @@ |
||||
import React from 'react' |
||||
import assert from 'assert' |
||||
import shallow from '../../../../../lib/shallow-with-context' |
||||
import SignatureRequest from '../signature-request.component' |
||||
|
||||
|
||||
describe('Signature Request Component', function () { |
||||
let wrapper |
||||
|
||||
beforeEach(() => { |
||||
wrapper = shallow(<SignatureRequest txData={{ |
||||
msgParams: { |
||||
data: '{"message": {"from": {"name": "hello"}}}', |
||||
from: '0x123456789abcdef', |
||||
} }} />) |
||||
}) |
||||
|
||||
describe('render', () => { |
||||
it('should render a div with one child', () => { |
||||
assert(wrapper.is('div')) |
||||
assert.equal(wrapper.length, 1) |
||||
assert(wrapper.hasClass('signature-request')) |
||||
}) |
||||
}) |
||||
}) |
@ -1 +1 @@ |
||||
export { default } from './transaction-list-item-details.component' |
||||
export { default } from './transaction-list-item-details.container' |
||||
|
@ -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) |
@ -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 |
||||
} |
@ -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) |
||||
} |
@ -0,0 +1,223 @@ |
||||
import React, { Component } from 'react' |
||||
import PropTypes from 'prop-types' |
||||
import classnames from 'classnames' |
||||
import Identicon from '../../../components/ui/identicon' |
||||
import { |
||||
addressSummary, |
||||
} from '../../../helpers/utils/util' |
||||
|
||||
export default class ConfirmApproveContent extends Component { |
||||
static contextTypes = { |
||||
t: PropTypes.func, |
||||
} |
||||
|
||||
static propTypes = { |
||||
amount: PropTypes.string, |
||||
txFeeTotal: PropTypes.string, |
||||
tokenAmount: PropTypes.string, |
||||
customTokenAmount: PropTypes.string, |
||||
tokenSymbol: PropTypes.string, |
||||
siteImage: PropTypes.string, |
||||
tokenAddress: PropTypes.string, |
||||
showCustomizeGasModal: PropTypes.func, |
||||
showEditApprovalPermissionModal: PropTypes.func, |
||||
origin: PropTypes.string, |
||||
setCustomAmount: PropTypes.func, |
||||
tokenBalance: PropTypes.string, |
||||
data: PropTypes.string, |
||||
toAddress: PropTypes.string, |
||||
fiatTransactionTotal: PropTypes.string, |
||||
ethTransactionTotal: PropTypes.string, |
||||
} |
||||
|
||||
state = { |
||||
showFullTxDetails: false, |
||||
} |
||||
|
||||
renderApproveContentCard ({ |
||||
symbol, |
||||
title, |
||||
showEdit, |
||||
onEditClick, |
||||
content, |
||||
footer, |
||||
noBorder, |
||||
}) { |
||||
return ( |
||||
<div className={classnames({ |
||||
'confirm-approve-content__card': !noBorder, |
||||
'confirm-approve-content__card--no-border': noBorder, |
||||
})}> |
||||
<div className="confirm-approve-content__card-header"> |
||||
<div className="confirm-approve-content__card-header__symbol">{ symbol }</div> |
||||
<div className="confirm-approve-content__card-header__title">{ title }</div> |
||||
{ showEdit && <div |
||||
className="confirm-approve-content__small-blue-text cursor-pointer" |
||||
onClick={() => onEditClick()} |
||||
>Edit</div> } |
||||
</div> |
||||
<div className="confirm-approve-content__card-content"> |
||||
{ content } |
||||
</div> |
||||
{ footer } |
||||
</div> |
||||
) |
||||
} |
||||
|
||||
// TODO: Add "Learn Why" with link to the feeAssociatedRequest text
|
||||
renderTransactionDetailsContent () { |
||||
const { t } = this.context |
||||
const { |
||||
ethTransactionTotal, |
||||
fiatTransactionTotal, |
||||
} = this.props |
||||
return ( |
||||
<div className="confirm-approve-content__transaction-details-content"> |
||||
<div className="confirm-approve-content__small-text"> |
||||
{ t('feeAssociatedRequest') } |
||||
</div> |
||||
<div className="confirm-approve-content__transaction-details-content__fee"> |
||||
<div className="confirm-approve-content__transaction-details-content__primary-fee"> |
||||
{ fiatTransactionTotal } |
||||
</div> |
||||
<div className="confirm-approve-content__transaction-details-content__secondary-fee"> |
||||
{ ethTransactionTotal } |
||||
</div> |
||||
</div> |
||||
</div> |
||||
) |
||||
} |
||||
|
||||
renderPermissionContent () { |
||||
const { t } = this.context |
||||
const { customTokenAmount, tokenAmount, tokenSymbol, origin, toAddress } = this.props |
||||
|
||||
return ( |
||||
<div className="flex-column"> |
||||
<div className="confirm-approve-content__small-text">{ t('accessAndSpendNotice', [origin]) }</div> |
||||
<div className="flex-row"> |
||||
<div className="confirm-approve-content__label">{ t('amountWithColon') }</div> |
||||
<div className="confirm-approve-content__medium-text">{ `${customTokenAmount || tokenAmount} ${tokenSymbol}` }</div> |
||||
</div> |
||||
<div className="flex-row"> |
||||
<div className="confirm-approve-content__label">{ t('toWithColon') }</div> |
||||
<div className="confirm-approve-content__medium-text">{ addressSummary(toAddress) }</div> |
||||
</div> |
||||
</div> |
||||
) |
||||
} |
||||
|
||||
renderDataContent () { |
||||
const { t } = this.context |
||||
const { data } = this.props |
||||
return ( |
||||
<div className="flex-column"> |
||||
<div className="confirm-approve-content__small-text">{ t('functionApprove') }</div> |
||||
<div className="confirm-approve-content__small-text confirm-approve-content__data__data-block">{ data }</div> |
||||
</div> |
||||
) |
||||
} |
||||
|
||||
render () { |
||||
const { t } = this.context |
||||
const { |
||||
siteImage, |
||||
tokenAmount, |
||||
customTokenAmount, |
||||
origin, |
||||
tokenSymbol, |
||||
showCustomizeGasModal, |
||||
showEditApprovalPermissionModal, |
||||
setCustomAmount, |
||||
tokenBalance, |
||||
} = this.props |
||||
const { showFullTxDetails } = this.state |
||||
|
||||
return ( |
||||
<div className={classnames('confirm-approve-content', { |
||||
'confirm-approve-content--full': showFullTxDetails, |
||||
})}> |
||||
<div className="confirm-approve-content__identicon-wrapper"> |
||||
<Identicon |
||||
className="confirm-approve-content__identicon" |
||||
diameter={48} |
||||
address={origin} |
||||
image={siteImage} |
||||
/> |
||||
</div> |
||||
<div className="confirm-approve-content__title"> |
||||
{ t('allowOriginSpendToken', [origin, tokenSymbol]) } |
||||
</div> |
||||
<div className="confirm-approve-content__description"> |
||||
{ t('trustSiteApprovePermission', [origin, tokenSymbol]) } |
||||
</div> |
||||
<div |
||||
className="confirm-approve-content__edit-submission-button-container" |
||||
> |
||||
<div |
||||
className="confirm-approve-content__medium-link-text cursor-pointer" |
||||
onClick={() => showEditApprovalPermissionModal({ customTokenAmount, tokenAmount, tokenSymbol, setCustomAmount, tokenBalance, origin })} |
||||
> |
||||
{ t('editPermission') } |
||||
</div> |
||||
</div> |
||||
<div className="confirm-approve-content__card-wrapper"> |
||||
{this.renderApproveContentCard({ |
||||
symbol: <i className="fa fa-tag" />, |
||||
title: 'Transaction Fee', |
||||
showEdit: true, |
||||
onEditClick: showCustomizeGasModal, |
||||
content: this.renderTransactionDetailsContent(), |
||||
noBorder: !showFullTxDetails, |
||||
footer: <div |
||||
className="confirm-approve-content__view-full-tx-button-wrapper" |
||||
onClick={() => this.setState({ showFullTxDetails: !this.state.showFullTxDetails })} |
||||
> |
||||
<div className="confirm-approve-content__view-full-tx-button cursor-pointer"> |
||||
<div className="confirm-approve-content__small-blue-text"> |
||||
View full transaction details |
||||
</div> |
||||
<i className={classnames({ |
||||
'fa fa-caret-up': showFullTxDetails, |
||||
'fa fa-caret-down': !showFullTxDetails, |
||||
})} /> |
||||
</div> |
||||
</div>, |
||||
})} |
||||
</div> |
||||
|
||||
{ |
||||
showFullTxDetails |
||||
? ( |
||||
<div className="confirm-approve-content__full-tx-content"> |
||||
<div className="confirm-approve-content__permission"> |
||||
{this.renderApproveContentCard({ |
||||
symbol: <img src="/images/user-check.svg" />, |
||||
title: 'Permission', |
||||
content: this.renderPermissionContent(), |
||||
showEdit: true, |
||||
onEditClick: () => showEditApprovalPermissionModal({ |
||||
customTokenAmount, |
||||
tokenAmount, |
||||
tokenSymbol, |
||||
tokenBalance, |
||||
setCustomAmount, |
||||
}), |
||||
})} |
||||
</div> |
||||
<div className="confirm-approve-content__data"> |
||||
{this.renderApproveContentCard({ |
||||
symbol: <i className="fa fa-file" />, |
||||
title: 'Data', |
||||
content: this.renderDataContent(), |
||||
noBorder: true, |
||||
})} |
||||
</div> |
||||
</div> |
||||
) |
||||
: null |
||||
} |
||||
</div> |
||||
) |
||||
} |
||||
} |
@ -0,0 +1 @@ |
||||
export { default } from './confirm-approve-content.component' |
@ -0,0 +1,306 @@ |
||||
.confirm-approve-content { |
||||
display: flex; |
||||
flex-flow: column; |
||||
align-items: center; |
||||
width: 100%; |
||||
height: 100%; |
||||
|
||||
font-family: Roboto; |
||||
font-style: normal; |
||||
|
||||
&__identicon-wrapper { |
||||
display: flex; |
||||
width: 100%; |
||||
justify-content: center; |
||||
margin-top: 22px; |
||||
padding-left: 24px; |
||||
padding-right: 24px; |
||||
} |
||||
|
||||
&__full-tx-content { |
||||
display: flex; |
||||
flex-flow: column; |
||||
align-items: center; |
||||
width: 390px; |
||||
font-family: Roboto; |
||||
font-style: normal; |
||||
padding-left: 24px; |
||||
padding-right: 24px; |
||||
} |
||||
|
||||
&__card-wrapper { |
||||
width: 100%; |
||||
} |
||||
|
||||
&__title { |
||||
font-weight: normal; |
||||
font-size: 24px; |
||||
line-height: 34px; |
||||
width: 100%; |
||||
display: flex; |
||||
justify-content: center; |
||||
text-align: center; |
||||
margin-top: 22px; |
||||
padding-left: 24px; |
||||
padding-right: 24px; |
||||
} |
||||
|
||||
&__description { |
||||
font-weight: normal; |
||||
font-size: 14px; |
||||
line-height: 20px; |
||||
margin-top: 16px; |
||||
margin-bottom: 16px; |
||||
color: #6A737D; |
||||
text-align: center; |
||||
padding-left: 24px; |
||||
padding-right: 24px; |
||||
} |
||||
|
||||
&__card, |
||||
&__card--no-border { |
||||
display: flex; |
||||
flex-flow: column; |
||||
border-bottom: 1px solid #D2D8DD; |
||||
position: relative; |
||||
padding-left: 24px; |
||||
padding-right: 24px; |
||||
|
||||
&__bold-text { |
||||
font-weight: bold; |
||||
font-size: 14px; |
||||
line-height: 20px; |
||||
} |
||||
|
||||
&__thin-text { |
||||
font-weight: normal; |
||||
font-size: 12px; |
||||
line-height: 17px; |
||||
color: #6A737D; |
||||
} |
||||
} |
||||
|
||||
&__card--no-border { |
||||
border-bottom: none; |
||||
} |
||||
|
||||
&__card-header { |
||||
display: flex; |
||||
flex-flow: row; |
||||
margin-top: 20px; |
||||
align-items: center; |
||||
position: relative; |
||||
|
||||
&__symbol { |
||||
width: auto; |
||||
} |
||||
|
||||
&__symbol--aligned { |
||||
width: 100%; |
||||
} |
||||
|
||||
&__title, &__title-value { |
||||
font-weight: bold; |
||||
font-size: 14px; |
||||
line-height: 20px; |
||||
} |
||||
|
||||
&__title { |
||||
width: 100%; |
||||
margin-left: 16px; |
||||
} |
||||
|
||||
&__title--aligned { |
||||
margin-left: 27px; |
||||
position: absolute; |
||||
width: auto; |
||||
} |
||||
} |
||||
|
||||
&__card-content { |
||||
margin-top: 6px; |
||||
margin-bottom: 12px; |
||||
} |
||||
|
||||
&__card-content--aligned { |
||||
margin-left: 42px; |
||||
} |
||||
|
||||
&__transaction-total-symbol { |
||||
width: 16px; |
||||
display: flex; |
||||
justify-content: center; |
||||
align-items: center; |
||||
height: 16px; |
||||
|
||||
&__x { |
||||
display: flex; |
||||
justify-content: center; |
||||
align-items: center; |
||||
|
||||
div { |
||||
width: 22px; |
||||
height: 2px; |
||||
background: #037DD6; |
||||
position: absolute; |
||||
} |
||||
|
||||
div:first-of-type { |
||||
transform: rotate(45deg); |
||||
} |
||||
|
||||
div:last-of-type { |
||||
transform: rotate(-45deg); |
||||
} |
||||
} |
||||
|
||||
&__circle { |
||||
width: 14px; |
||||
height: 14px; |
||||
border: 2px solid #037DD6; |
||||
border-radius: 50%; |
||||
background: white; |
||||
position: absolute; |
||||
} |
||||
} |
||||
|
||||
&__transaction-details-content { |
||||
display: flex; |
||||
flex-flow: row; |
||||
justify-content: space-between; |
||||
|
||||
.confirm-approve-content__small-text { |
||||
width: 160px; |
||||
} |
||||
|
||||
&__fee { |
||||
display: flex; |
||||
flex-flow: column; |
||||
align-items: flex-end; |
||||
text-align: right; |
||||
} |
||||
|
||||
&__primary-fee { |
||||
font-weight: bold; |
||||
font-size: 18px; |
||||
line-height: 25px; |
||||
color: #000000; |
||||
} |
||||
|
||||
&__secondary-fee { |
||||
font-weight: normal; |
||||
font-size: 14px; |
||||
line-height: 20px; |
||||
color: #8C8E94; |
||||
} |
||||
} |
||||
|
||||
&__view-full-tx-button-wrapper { |
||||
display: flex; |
||||
flex-flow: row; |
||||
margin-bottom: 16px; |
||||
justify-content: center; |
||||
|
||||
i { |
||||
margin-left: 6px; |
||||
display: flex; |
||||
color: #3099f2; |
||||
align-items: center; |
||||
} |
||||
} |
||||
|
||||
&__view-full-tx-button { |
||||
display: flex; |
||||
flex-flow: row; |
||||
} |
||||
|
||||
&__edit-submission-button-container { |
||||
display: flex; |
||||
flex-flow: row; |
||||
padding-top: 15px; |
||||
padding-bottom: 30px; |
||||
border-bottom: 1px solid #D2D8DD; |
||||
width: 100%; |
||||
justify-content: center; |
||||
padding-left: 24px; |
||||
padding-right: 24px; |
||||
} |
||||
|
||||
&__large-text { |
||||
font-size: 18px; |
||||
line-height: 25px; |
||||
color: #24292E; |
||||
} |
||||
|
||||
&__medium-link-text { |
||||
font-size: 14px; |
||||
line-height: 20px; |
||||
font-weight: 500; |
||||
color: #037DD6; |
||||
} |
||||
|
||||
&__medium-text, |
||||
&__label { |
||||
font-weight: normal; |
||||
font-size: 14px; |
||||
line-height: 20px; |
||||
color: #24292E; |
||||
} |
||||
|
||||
&__label { |
||||
font-weight: bold; |
||||
margin-right: 4px; |
||||
} |
||||
|
||||
&__small-text, &__small-blue-text, &__info-row { |
||||
font-weight: normal; |
||||
font-size: 12px; |
||||
line-height: 17px; |
||||
color: #6A737D; |
||||
} |
||||
|
||||
&__small-blue-text { |
||||
color: #037DD6; |
||||
} |
||||
|
||||
&__info-row { |
||||
display: flex; |
||||
justify-content: space-between; |
||||
margin-bottom: 6px; |
||||
} |
||||
|
||||
&__data, |
||||
&__permission { |
||||
width: 100%; |
||||
} |
||||
|
||||
&__permission { |
||||
.flex-row { |
||||
margin-top: 14px; |
||||
} |
||||
} |
||||
|
||||
&__data { |
||||
&__data-block { |
||||
overflow-wrap: break-word; |
||||
margin-right: 16px; |
||||
margin-top: 12px; |
||||
} |
||||
} |
||||
|
||||
&__footer { |
||||
display: flex; |
||||
align-items: flex-end; |
||||
margin-top: 16px; |
||||
padding-left: 34px; |
||||
padding-right: 24px; |
||||
|
||||
.confirm-approve-content__small-text { |
||||
margin-left: 16px; |
||||
} |
||||
} |
||||
} |
||||
|
||||
.confirm-approve-content--full { |
||||
height: auto; |
||||
} |
@ -1,15 +1,102 @@ |
||||
import { connect } from 'react-redux' |
||||
import { compose } from 'recompose' |
||||
import { withRouter } from 'react-router-dom' |
||||
import { |
||||
contractExchangeRateSelector, |
||||
transactionFeeSelector, |
||||
} from '../../selectors/confirm-transaction' |
||||
import { showModal } from '../../store/actions' |
||||
import { tokenSelector } from '../../selectors/tokens' |
||||
import { |
||||
getTokenData, |
||||
} from '../../helpers/utils/transactions.util' |
||||
import withTokenTracker from '../../helpers/higher-order-components/with-token-tracker' |
||||
import { |
||||
calcTokenAmount, |
||||
getTokenToAddress, |
||||
getTokenValue, |
||||
} from '../../helpers/utils/token-util' |
||||
import ConfirmApprove from './confirm-approve.component' |
||||
import { approveTokenAmountAndToAddressSelector } from '../../selectors/confirm-transaction' |
||||
|
||||
const mapStateToProps = state => { |
||||
const { confirmTransaction: { tokenProps: { tokenSymbol } = {} } } = state |
||||
const { tokenAmount } = approveTokenAmountAndToAddressSelector(state) |
||||
const mapStateToProps = (state, ownProps) => { |
||||
const { match: { params = {} } } = ownProps |
||||
const { id: paramsTransactionId } = params |
||||
const { |
||||
confirmTransaction, |
||||
metamask: { currentCurrency, conversionRate, selectedAddressTxList, approvedOrigins, selectedAddress }, |
||||
} = state |
||||
|
||||
const { |
||||
txData: { id: transactionId, txParams: { to: tokenAddress, data } = {} } = {}, |
||||
} = confirmTransaction |
||||
|
||||
const transaction = selectedAddressTxList.find(({ id }) => id === (Number(paramsTransactionId) || transactionId)) || {} |
||||
|
||||
const { |
||||
ethTransactionTotal, |
||||
fiatTransactionTotal, |
||||
} = transactionFeeSelector(state, transaction) |
||||
const tokens = tokenSelector(state) |
||||
const currentToken = tokens && tokens.find(({ address }) => tokenAddress === address) |
||||
const { decimals, symbol: tokenSymbol } = currentToken || {} |
||||
|
||||
const tokenData = getTokenData(data) |
||||
const tokenValue = tokenData && getTokenValue(tokenData.params) |
||||
const toAddress = tokenData && getTokenToAddress(tokenData.params) |
||||
const tokenAmount = tokenData && calcTokenAmount(tokenValue, decimals).toNumber() |
||||
const contractExchangeRate = contractExchangeRateSelector(state) |
||||
|
||||
const { origin } = transaction |
||||
const formattedOrigin = origin |
||||
? origin[0].toUpperCase() + origin.slice(1) |
||||
: '' |
||||
|
||||
const { siteImage } = approvedOrigins[origin] || {} |
||||
return { |
||||
toAddress, |
||||
tokenAddress, |
||||
tokenAmount, |
||||
currentCurrency, |
||||
conversionRate, |
||||
contractExchangeRate, |
||||
fiatTransactionTotal, |
||||
ethTransactionTotal, |
||||
tokenSymbol, |
||||
siteImage, |
||||
token: { address: tokenAddress }, |
||||
userAddress: selectedAddress, |
||||
origin: formattedOrigin, |
||||
data, |
||||
decimals: Number(decimals), |
||||
txData: transaction, |
||||
} |
||||
} |
||||
|
||||
export default connect(mapStateToProps)(ConfirmApprove) |
||||
const mapDispatchToProps = (dispatch) => { |
||||
return { |
||||
showCustomizeGasModal: (txData) => dispatch(showModal({ name: 'CUSTOMIZE_GAS', txData })), |
||||
showEditApprovalPermissionModal: ({ |
||||
tokenAmount, |
||||
customTokenAmount, |
||||
tokenSymbol, |
||||
tokenBalance, |
||||
setCustomAmount, |
||||
origin, |
||||
}) => dispatch(showModal({ |
||||
name: 'EDIT_APPROVAL_PERMISSION', |
||||
tokenAmount, |
||||
customTokenAmount, |
||||
tokenSymbol, |
||||
tokenBalance, |
||||
setCustomAmount, |
||||
origin, |
||||
})), |
||||
} |
||||
} |
||||
|
||||
export default compose( |
||||
withRouter, |
||||
connect(mapStateToProps, mapDispatchToProps), |
||||
withTokenTracker, |
||||
)(ConfirmApprove) |
||||
|
||||
|
@ -0,0 +1,28 @@ |
||||
import { decimalToHex } from '../../helpers/utils/conversions.util' |
||||
import { calcTokenValue } from '../../helpers/utils/token-util.js' |
||||
|
||||
export function getCustomTxParamsData (data, { customPermissionAmount, tokenAmount, decimals }) { |
||||
if (customPermissionAmount) { |
||||
const tokenValue = decimalToHex(calcTokenValue(tokenAmount, decimals)) |
||||
|
||||
const re = new RegExp('(^.+)' + tokenValue + '$') |
||||
const matches = re.exec(data) |
||||
|
||||
if (!matches || !matches[1]) { |
||||
return data |
||||
} |
||||
let dataWithoutCurrentAmount = matches[1] |
||||
const customPermissionValue = decimalToHex(calcTokenValue(Number(customPermissionAmount), decimals)) |
||||
|
||||
const differenceInLengths = customPermissionValue.length - tokenValue.length |
||||
const zeroModifier = dataWithoutCurrentAmount.length - differenceInLengths |
||||
if (differenceInLengths > 0) { |
||||
dataWithoutCurrentAmount = dataWithoutCurrentAmount.slice(0, zeroModifier) |
||||
} else if (differenceInLengths < 0) { |
||||
dataWithoutCurrentAmount = dataWithoutCurrentAmount.padEnd(zeroModifier, 0) |
||||
} |
||||
|
||||
const customTxParamsData = dataWithoutCurrentAmount + customPermissionValue |
||||
return customTxParamsData |
||||
} |
||||
} |
@ -0,0 +1 @@ |
||||
@import 'confirm-approve-content/index'; |
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in new issue