mirror of
https://github.com/kremalicious/metamask-extension.git
synced 2024-12-23 09:52:26 +01:00
Merge pull request #4606 from MetaMask/WatchTokenFeature
Add metamask_watchAsset
This commit is contained in:
commit
4560df6e73
@ -2,6 +2,8 @@
|
||||
|
||||
## Current Develop Branch
|
||||
|
||||
- (#4606)[https://github.com/MetaMask/metamask-extension/pull/4606]: Add new metamask_watchAsset method.
|
||||
|
||||
## 4.9.3 Wed Aug 15 2018
|
||||
|
||||
- (#4897)[https://github.com/MetaMask/metamask-extension/pull/4897]: QR code scan for recipient addresses.
|
||||
|
@ -29,6 +29,9 @@
|
||||
"addTokens": {
|
||||
"message": "Add Tokens"
|
||||
},
|
||||
"addSuggestedTokens": {
|
||||
"message": "Add Suggested Tokens"
|
||||
},
|
||||
"addAcquiredTokens": {
|
||||
"message": "Add the tokens you've acquired using MetaMask"
|
||||
},
|
||||
|
@ -256,6 +256,7 @@ function setupController (initState, initLangCode) {
|
||||
showUnconfirmedMessage: triggerUi,
|
||||
unlockAccountMessage: triggerUi,
|
||||
showUnapprovedTx: triggerUi,
|
||||
showWatchAssetUi: showWatchAssetUi,
|
||||
// initial state
|
||||
initState,
|
||||
// initial locale code
|
||||
@ -443,9 +444,28 @@ function triggerUi () {
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens the browser popup for user confirmation of watchAsset
|
||||
* then it waits until user interact with the UI
|
||||
*/
|
||||
function showWatchAssetUi () {
|
||||
triggerUi()
|
||||
return new Promise(
|
||||
(resolve) => {
|
||||
var interval = setInterval(() => {
|
||||
if (!notificationIsOpen) {
|
||||
clearInterval(interval)
|
||||
resolve()
|
||||
}
|
||||
}, 1000)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
// On first install, open a window to MetaMask website to how-it-works.
|
||||
extension.runtime.onInstalled.addListener(function (details) {
|
||||
if ((details.reason === 'install') && (!METAMASK_DEBUG)) {
|
||||
extension.tabs.create({url: 'https://metamask.io/#how-it-works'})
|
||||
}
|
||||
})
|
||||
|
||||
|
@ -1,5 +1,6 @@
|
||||
const ObservableStore = require('obs-store')
|
||||
const normalizeAddress = require('eth-sig-util').normalize
|
||||
const { isValidAddress } = require('ethereumjs-util')
|
||||
const extend = require('xtend')
|
||||
|
||||
|
||||
@ -14,6 +15,7 @@ class PreferencesController {
|
||||
* @property {string} store.currentAccountTab Indicates the selected tab in the ui
|
||||
* @property {array} store.tokens The tokens the user wants display in their token lists
|
||||
* @property {object} store.accountTokens The tokens stored per account and then per network type
|
||||
* @property {object} store.assetImages Contains assets objects related to assets added
|
||||
* @property {boolean} store.useBlockie The users preference for blockie identicons within the UI
|
||||
* @property {object} store.featureFlags A key-boolean map, where keys refer to features and booleans to whether the
|
||||
* user wishes to see that feature
|
||||
@ -26,7 +28,9 @@ class PreferencesController {
|
||||
frequentRpcList: [],
|
||||
currentAccountTab: 'history',
|
||||
accountTokens: {},
|
||||
assetImages: {},
|
||||
tokens: [],
|
||||
suggestedTokens: {},
|
||||
useBlockie: false,
|
||||
featureFlags: {},
|
||||
currentLocale: opts.initLangCode,
|
||||
@ -37,6 +41,7 @@ class PreferencesController {
|
||||
this.diagnostics = opts.diagnostics
|
||||
this.network = opts.network
|
||||
this.store = new ObservableStore(initState)
|
||||
this.showWatchAssetUi = opts.showWatchAssetUi
|
||||
this._subscribeProviderType()
|
||||
}
|
||||
// PUBLIC METHODS
|
||||
@ -51,6 +56,53 @@ class PreferencesController {
|
||||
this.store.updateState({ useBlockie: val })
|
||||
}
|
||||
|
||||
getSuggestedTokens () {
|
||||
return this.store.getState().suggestedTokens
|
||||
}
|
||||
|
||||
getAssetImages () {
|
||||
return this.store.getState().assetImages
|
||||
}
|
||||
|
||||
addSuggestedERC20Asset (tokenOpts) {
|
||||
this._validateERC20AssetParams(tokenOpts)
|
||||
const suggested = this.getSuggestedTokens()
|
||||
const { rawAddress, symbol, decimals, image } = tokenOpts
|
||||
const address = normalizeAddress(rawAddress)
|
||||
const newEntry = { address, symbol, decimals, image }
|
||||
suggested[address] = newEntry
|
||||
this.store.updateState({ suggestedTokens: suggested })
|
||||
}
|
||||
|
||||
/**
|
||||
* RPC engine middleware for requesting new asset added
|
||||
*
|
||||
* @param req
|
||||
* @param res
|
||||
* @param {Function} - next
|
||||
* @param {Function} - end
|
||||
*/
|
||||
async requestWatchAsset (req, res, next, end) {
|
||||
if (req.method === 'metamask_watchAsset') {
|
||||
const { type, options } = req.params
|
||||
switch (type) {
|
||||
case 'ERC20':
|
||||
const result = await this._handleWatchAssetERC20(options)
|
||||
if (result instanceof Error) {
|
||||
end(result)
|
||||
} else {
|
||||
res.result = result
|
||||
end()
|
||||
}
|
||||
break
|
||||
default:
|
||||
end(new Error(`Asset of type ${type} not supported`))
|
||||
}
|
||||
} else {
|
||||
next()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Getter for the `useBlockie` property
|
||||
*
|
||||
@ -186,6 +238,13 @@ class PreferencesController {
|
||||
return selected
|
||||
}
|
||||
|
||||
removeSuggestedTokens () {
|
||||
return new Promise((resolve, reject) => {
|
||||
this.store.updateState({ suggestedTokens: {} })
|
||||
resolve({})
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Setter for the `selectedAddress` property
|
||||
*
|
||||
@ -232,11 +291,11 @@ class PreferencesController {
|
||||
* @returns {Promise<array>} Promises the new array of AddedToken objects.
|
||||
*
|
||||
*/
|
||||
async addToken (rawAddress, symbol, decimals) {
|
||||
async addToken (rawAddress, symbol, decimals, image) {
|
||||
const address = normalizeAddress(rawAddress)
|
||||
const newEntry = { address, symbol, decimals }
|
||||
|
||||
const tokens = this.store.getState().tokens
|
||||
const assetImages = this.getAssetImages()
|
||||
const previousEntry = tokens.find((token, index) => {
|
||||
return token.address === address
|
||||
})
|
||||
@ -247,7 +306,8 @@ class PreferencesController {
|
||||
} else {
|
||||
tokens.push(newEntry)
|
||||
}
|
||||
this._updateAccountTokens(tokens)
|
||||
assetImages[address] = image
|
||||
this._updateAccountTokens(tokens, assetImages)
|
||||
return Promise.resolve(tokens)
|
||||
}
|
||||
|
||||
@ -260,8 +320,10 @@ class PreferencesController {
|
||||
*/
|
||||
removeToken (rawAddress) {
|
||||
const tokens = this.store.getState().tokens
|
||||
const assetImages = this.getAssetImages()
|
||||
const updatedTokens = tokens.filter(token => token.address !== rawAddress)
|
||||
this._updateAccountTokens(updatedTokens)
|
||||
delete assetImages[rawAddress]
|
||||
this._updateAccountTokens(updatedTokens, assetImages)
|
||||
return Promise.resolve(updatedTokens)
|
||||
}
|
||||
|
||||
@ -387,6 +449,7 @@ class PreferencesController {
|
||||
//
|
||||
// PRIVATE METHODS
|
||||
//
|
||||
|
||||
/**
|
||||
* Subscription to network provider type.
|
||||
*
|
||||
@ -405,10 +468,10 @@ class PreferencesController {
|
||||
* @param {array} tokens Array of tokens to be updated.
|
||||
*
|
||||
*/
|
||||
_updateAccountTokens (tokens) {
|
||||
_updateAccountTokens (tokens, assetImages) {
|
||||
const { accountTokens, providerType, selectedAddress } = this._getTokenRelatedStates()
|
||||
accountTokens[selectedAddress][providerType] = tokens
|
||||
this.store.updateState({ accountTokens, tokens })
|
||||
this.store.updateState({ accountTokens, tokens, assetImages })
|
||||
}
|
||||
|
||||
/**
|
||||
@ -438,6 +501,47 @@ class PreferencesController {
|
||||
const tokens = accountTokens[selectedAddress][providerType]
|
||||
return { tokens, accountTokens, providerType, selectedAddress }
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle the suggestion of an ERC20 asset through `watchAsset`
|
||||
* *
|
||||
* @param {Promise} promise Promise according to addition of ERC20 token
|
||||
*
|
||||
*/
|
||||
async _handleWatchAssetERC20 (options) {
|
||||
const { address, symbol, decimals, image } = options
|
||||
const rawAddress = address
|
||||
try {
|
||||
this._validateERC20AssetParams({ rawAddress, symbol, decimals })
|
||||
} catch (err) {
|
||||
return err
|
||||
}
|
||||
const tokenOpts = { rawAddress, decimals, symbol, image }
|
||||
this.addSuggestedERC20Asset(tokenOpts)
|
||||
return this.showWatchAssetUi().then(() => {
|
||||
const tokenAddresses = this.getTokens().filter(token => token.address === normalizeAddress(rawAddress))
|
||||
return tokenAddresses.length > 0
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates that the passed options for suggested token have all required properties.
|
||||
*
|
||||
* @param {Object} opts The options object to validate
|
||||
* @throws {string} Throw a custom error indicating that address, symbol and/or decimals
|
||||
* doesn't fulfill requirements
|
||||
*
|
||||
*/
|
||||
_validateERC20AssetParams (opts) {
|
||||
const { rawAddress, symbol, decimals } = opts
|
||||
if (!rawAddress || !symbol || !decimals) throw new Error(`Cannot suggest token without address, symbol, and decimals`)
|
||||
if (!(symbol.length < 6)) throw new Error(`Invalid symbol ${symbol} more than five characters`)
|
||||
const numDecimals = parseInt(decimals, 10)
|
||||
if (isNaN(numDecimals) || numDecimals > 36 || numDecimals < 0) {
|
||||
throw new Error(`Invalid decimals ${decimals} must be at least 0, and not over 36`)
|
||||
}
|
||||
if (!isValidAddress(rawAddress)) throw new Error(`Invalid address ${rawAddress}`)
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = PreferencesController
|
||||
|
@ -92,6 +92,7 @@ module.exports = class MetamaskController extends EventEmitter {
|
||||
this.preferencesController = new PreferencesController({
|
||||
initState: initState.PreferencesController,
|
||||
initLangCode: opts.initLangCode,
|
||||
showWatchAssetUi: opts.showWatchAssetUi,
|
||||
network: this.networkController,
|
||||
})
|
||||
|
||||
@ -386,6 +387,7 @@ module.exports = class MetamaskController extends EventEmitter {
|
||||
setSelectedAddress: nodeify(preferencesController.setSelectedAddress, preferencesController),
|
||||
addToken: nodeify(preferencesController.addToken, preferencesController),
|
||||
removeToken: nodeify(preferencesController.removeToken, preferencesController),
|
||||
removeSuggestedTokens: nodeify(preferencesController.removeSuggestedTokens, preferencesController),
|
||||
setCurrentAccountTab: nodeify(preferencesController.setCurrentAccountTab, preferencesController),
|
||||
setAccountLabel: nodeify(preferencesController.setAccountLabel, preferencesController),
|
||||
setFeatureFlag: nodeify(preferencesController.setFeatureFlag, preferencesController),
|
||||
@ -1250,6 +1252,7 @@ module.exports = class MetamaskController extends EventEmitter {
|
||||
engine.push(createOriginMiddleware({ origin }))
|
||||
engine.push(createLoggerMiddleware({ origin }))
|
||||
engine.push(filterMiddleware)
|
||||
engine.push(this.preferencesController.requestWatchAsset.bind(this.preferencesController))
|
||||
engine.push(createProviderMiddleware({ provider: this.provider }))
|
||||
|
||||
// setup connection
|
||||
|
@ -32,6 +32,7 @@ function mapStateToProps (state) {
|
||||
currentCurrency: state.metamask.currentCurrency,
|
||||
currentAccountTab: state.metamask.currentAccountTab,
|
||||
tokens: state.metamask.tokens,
|
||||
suggestedTokens: state.metamask.suggestedTokens,
|
||||
computedBalances: state.metamask.computedBalances,
|
||||
}
|
||||
}
|
||||
@ -49,6 +50,10 @@ AccountDetailScreen.prototype.render = function () {
|
||||
var account = props.accounts[selected]
|
||||
const { network, conversionRate, currentCurrency } = props
|
||||
|
||||
if (Object.keys(props.suggestedTokens).length > 0) {
|
||||
this.props.dispatch(actions.showAddSuggestedTokenPage())
|
||||
}
|
||||
|
||||
return (
|
||||
|
||||
h('.account-detail-section.full-flex-height', [
|
||||
|
202
old-ui/app/add-suggested-token.js
Normal file
202
old-ui/app/add-suggested-token.js
Normal file
@ -0,0 +1,202 @@
|
||||
const inherits = require('util').inherits
|
||||
const Component = require('react').Component
|
||||
const h = require('react-hyperscript')
|
||||
const connect = require('react-redux').connect
|
||||
const actions = require('../../ui/app/actions')
|
||||
const Tooltip = require('./components/tooltip.js')
|
||||
const ethUtil = require('ethereumjs-util')
|
||||
const Copyable = require('./components/copyable')
|
||||
const addressSummary = require('./util').addressSummary
|
||||
|
||||
|
||||
module.exports = connect(mapStateToProps)(AddSuggestedTokenScreen)
|
||||
|
||||
function mapStateToProps (state) {
|
||||
return {
|
||||
identities: state.metamask.identities,
|
||||
suggestedTokens: state.metamask.suggestedTokens,
|
||||
}
|
||||
}
|
||||
|
||||
inherits(AddSuggestedTokenScreen, Component)
|
||||
function AddSuggestedTokenScreen () {
|
||||
this.state = {
|
||||
warning: null,
|
||||
}
|
||||
Component.call(this)
|
||||
}
|
||||
|
||||
AddSuggestedTokenScreen.prototype.render = function () {
|
||||
const state = this.state
|
||||
const props = this.props
|
||||
const { warning } = state
|
||||
const key = Object.keys(props.suggestedTokens)[0]
|
||||
const { address, symbol, decimals } = props.suggestedTokens[key]
|
||||
|
||||
return (
|
||||
h('.flex-column.flex-grow', [
|
||||
|
||||
// subtitle and nav
|
||||
h('.section-title.flex-row.flex-center', [
|
||||
h('h2.page-subtitle', 'Add Suggested Token'),
|
||||
]),
|
||||
|
||||
h('.error', {
|
||||
style: {
|
||||
display: warning ? 'block' : 'none',
|
||||
padding: '0 20px',
|
||||
textAlign: 'center',
|
||||
},
|
||||
}, warning),
|
||||
|
||||
// conf view
|
||||
h('.flex-column.flex-justify-center.flex-grow.select-none', [
|
||||
h('.flex-space-around', {
|
||||
style: {
|
||||
padding: '20px',
|
||||
},
|
||||
}, [
|
||||
|
||||
h('div', [
|
||||
h(Tooltip, {
|
||||
position: 'top',
|
||||
title: 'The contract of the actual token contract. Click for more info.',
|
||||
}, [
|
||||
h('a', {
|
||||
style: { fontWeight: 'bold', paddingRight: '10px'},
|
||||
href: 'https://support.metamask.io/kb/article/24-what-is-a-token-contract-address',
|
||||
target: '_blank',
|
||||
}, [
|
||||
h('span', 'Token Contract Address '),
|
||||
h('i.fa.fa-question-circle'),
|
||||
]),
|
||||
]),
|
||||
]),
|
||||
|
||||
h('div', {
|
||||
style: { display: 'flex' },
|
||||
}, [
|
||||
h(Copyable, {
|
||||
value: ethUtil.toChecksumAddress(address),
|
||||
}, [
|
||||
h('span#token-address', {
|
||||
style: {
|
||||
width: 'inherit',
|
||||
flex: '1 0 auto',
|
||||
height: '30px',
|
||||
margin: '8px',
|
||||
display: 'flex',
|
||||
},
|
||||
}, addressSummary(address, 24, 4, false)),
|
||||
]),
|
||||
]),
|
||||
|
||||
h('div', [
|
||||
h('span', {
|
||||
style: { fontWeight: 'bold', paddingRight: '10px'},
|
||||
}, 'Token Symbol'),
|
||||
]),
|
||||
|
||||
h('div', { style: {display: 'flex'} }, [
|
||||
h('p#token_symbol', {
|
||||
style: {
|
||||
width: 'inherit',
|
||||
flex: '1 0 auto',
|
||||
height: '30px',
|
||||
margin: '8px',
|
||||
},
|
||||
}, symbol),
|
||||
]),
|
||||
|
||||
h('div', [
|
||||
h('span', {
|
||||
style: { fontWeight: 'bold', paddingRight: '10px'},
|
||||
}, 'Decimals of Precision'),
|
||||
]),
|
||||
|
||||
h('div', { style: {display: 'flex'} }, [
|
||||
h('p#token_decimals', {
|
||||
type: 'number',
|
||||
style: {
|
||||
width: 'inherit',
|
||||
flex: '1 0 auto',
|
||||
height: '30px',
|
||||
margin: '8px',
|
||||
},
|
||||
}, decimals),
|
||||
]),
|
||||
|
||||
h('button', {
|
||||
style: {
|
||||
alignSelf: 'center',
|
||||
margin: '8px',
|
||||
},
|
||||
onClick: (event) => {
|
||||
this.props.dispatch(actions.removeSuggestedTokens())
|
||||
},
|
||||
}, 'Cancel'),
|
||||
|
||||
h('button', {
|
||||
style: {
|
||||
alignSelf: 'center',
|
||||
margin: '8px',
|
||||
},
|
||||
onClick: (event) => {
|
||||
const valid = this.validateInputs({ address, symbol, decimals })
|
||||
if (!valid) return
|
||||
|
||||
this.props.dispatch(actions.addToken(address.trim(), symbol.trim(), decimals))
|
||||
.then(() => {
|
||||
this.props.dispatch(actions.removeSuggestedTokens())
|
||||
})
|
||||
},
|
||||
}, 'Add'),
|
||||
]),
|
||||
]),
|
||||
])
|
||||
)
|
||||
}
|
||||
|
||||
AddSuggestedTokenScreen.prototype.componentWillMount = function () {
|
||||
if (typeof global.ethereumProvider === 'undefined') return
|
||||
}
|
||||
|
||||
AddSuggestedTokenScreen.prototype.validateInputs = function (opts) {
|
||||
let msg = ''
|
||||
const identitiesList = Object.keys(this.props.identities)
|
||||
const { address, symbol, decimals } = opts
|
||||
const standardAddress = ethUtil.addHexPrefix(address).toLowerCase()
|
||||
|
||||
const validAddress = ethUtil.isValidAddress(address)
|
||||
if (!validAddress) {
|
||||
msg += 'Address is invalid.'
|
||||
}
|
||||
|
||||
const validDecimals = decimals >= 0 && decimals < 36
|
||||
if (!validDecimals) {
|
||||
msg += 'Decimals must be at least 0, and not over 36. '
|
||||
}
|
||||
|
||||
const symbolLen = symbol.trim().length
|
||||
const validSymbol = symbolLen > 0 && symbolLen < 10
|
||||
if (!validSymbol) {
|
||||
msg += 'Symbol must be between 0 and 10 characters.'
|
||||
}
|
||||
|
||||
const ownAddress = identitiesList.includes(standardAddress)
|
||||
if (ownAddress) {
|
||||
msg = 'Personal address detected. Input the token contract address.'
|
||||
}
|
||||
|
||||
const isValid = validAddress && validDecimals && !ownAddress
|
||||
|
||||
if (!isValid) {
|
||||
this.setState({
|
||||
warning: msg,
|
||||
})
|
||||
} else {
|
||||
this.setState({ warning: null })
|
||||
}
|
||||
|
||||
return isValid
|
||||
}
|
@ -23,6 +23,7 @@ const generateLostAccountsNotice = require('../lib/lost-accounts-notice')
|
||||
// other views
|
||||
const ConfigScreen = require('./config')
|
||||
const AddTokenScreen = require('./add-token')
|
||||
const AddSuggestedTokenScreen = require('./add-suggested-token')
|
||||
const Import = require('./accounts/import')
|
||||
const InfoScreen = require('./info')
|
||||
const NewUiAnnouncement = require('./new-ui-annoucement')
|
||||
@ -74,6 +75,7 @@ function mapStateToProps (state) {
|
||||
lostAccounts: state.metamask.lostAccounts,
|
||||
frequentRpcList: state.metamask.frequentRpcList || [],
|
||||
featureFlags,
|
||||
suggestedTokens: state.metamask.suggestedTokens,
|
||||
|
||||
// state needed to get account dropdown temporarily rendering from app bar
|
||||
identities,
|
||||
@ -236,6 +238,10 @@ App.prototype.renderPrimary = function () {
|
||||
log.debug('rendering add-token screen from unlock screen.')
|
||||
return h(AddTokenScreen, {key: 'add-token'})
|
||||
|
||||
case 'add-suggested-token':
|
||||
log.debug('rendering add-suggested-token screen from unlock screen.')
|
||||
return h(AddSuggestedTokenScreen, {key: 'add-suggested-token'})
|
||||
|
||||
case 'config':
|
||||
log.debug('rendering config screen')
|
||||
return h(ConfigScreen, {key: 'config'})
|
||||
|
12252
package-lock.json
generated
12252
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -1,6 +1,7 @@
|
||||
const assert = require('assert')
|
||||
const ObservableStore = require('obs-store')
|
||||
const PreferencesController = require('../../../../app/scripts/controllers/preferences')
|
||||
const sinon = require('sinon')
|
||||
|
||||
describe('preferences controller', function () {
|
||||
let preferencesController
|
||||
@ -339,5 +340,114 @@ describe('preferences controller', function () {
|
||||
assert.deepEqual(tokensSecond, initialTokensSecond, 'tokens equal for same network')
|
||||
})
|
||||
})
|
||||
|
||||
describe('on watchAsset', function () {
|
||||
var stubNext, stubEnd, stubHandleWatchAssetERC20, asy, req, res
|
||||
const sandbox = sinon.createSandbox()
|
||||
|
||||
beforeEach(() => {
|
||||
req = {params: {}}
|
||||
res = {}
|
||||
asy = {next: () => {}, end: () => {}}
|
||||
stubNext = sandbox.stub(asy, 'next')
|
||||
stubEnd = sandbox.stub(asy, 'end').returns(0)
|
||||
stubHandleWatchAssetERC20 = sandbox.stub(preferencesController, '_handleWatchAssetERC20')
|
||||
})
|
||||
after(() => {
|
||||
sandbox.restore()
|
||||
})
|
||||
|
||||
it('shouldn not do anything if method not corresponds', async function () {
|
||||
const asy = {next: () => {}, end: () => {}}
|
||||
var stubNext = sandbox.stub(asy, 'next')
|
||||
var stubEnd = sandbox.stub(asy, 'end').returns(0)
|
||||
req.method = 'metamask'
|
||||
await preferencesController.requestWatchAsset(req, res, asy.next, asy.end)
|
||||
sandbox.assert.notCalled(stubEnd)
|
||||
sandbox.assert.called(stubNext)
|
||||
})
|
||||
it('should do something if method is supported', async function () {
|
||||
const asy = {next: () => {}, end: () => {}}
|
||||
var stubNext = sandbox.stub(asy, 'next')
|
||||
var stubEnd = sandbox.stub(asy, 'end').returns(0)
|
||||
req.method = 'metamask_watchAsset'
|
||||
req.params.type = 'someasset'
|
||||
await preferencesController.requestWatchAsset(req, res, asy.next, asy.end)
|
||||
sandbox.assert.called(stubEnd)
|
||||
sandbox.assert.notCalled(stubNext)
|
||||
})
|
||||
it('should through error if method is supported but asset type is not', async function () {
|
||||
req.method = 'metamask_watchAsset'
|
||||
req.params.type = 'someasset'
|
||||
await preferencesController.requestWatchAsset(req, res, asy.next, asy.end)
|
||||
sandbox.assert.called(stubEnd)
|
||||
sandbox.assert.notCalled(stubHandleWatchAssetERC20)
|
||||
sandbox.assert.notCalled(stubNext)
|
||||
assert.deepEqual(res, {})
|
||||
})
|
||||
it('should trigger handle add asset if type supported', async function () {
|
||||
const asy = {next: () => {}, end: () => {}}
|
||||
req.method = 'metamask_watchAsset'
|
||||
req.params.type = 'ERC20'
|
||||
await preferencesController.requestWatchAsset(req, res, asy.next, asy.end)
|
||||
sandbox.assert.called(stubHandleWatchAssetERC20)
|
||||
})
|
||||
})
|
||||
|
||||
describe('on watchAsset of type ERC20', function () {
|
||||
var req
|
||||
|
||||
const sandbox = sinon.createSandbox()
|
||||
beforeEach(() => {
|
||||
req = {params: {type: 'ERC20'}}
|
||||
})
|
||||
after(() => {
|
||||
sandbox.restore()
|
||||
})
|
||||
|
||||
it('should add suggested token', async function () {
|
||||
const address = '0xabcdef1234567'
|
||||
const symbol = 'ABBR'
|
||||
const decimals = 5
|
||||
const image = 'someimage'
|
||||
req.params.options = { address, symbol, decimals, image }
|
||||
|
||||
sandbox.stub(preferencesController, '_validateERC20AssetParams').returns(true)
|
||||
preferencesController.showWatchAssetUi = async () => {}
|
||||
|
||||
await preferencesController._handleWatchAssetERC20(req.params.options)
|
||||
const suggested = preferencesController.getSuggestedTokens()
|
||||
assert.equal(Object.keys(suggested).length, 1, `one token added ${Object.keys(suggested)}`)
|
||||
|
||||
assert.equal(suggested[address].address, address, 'set address correctly')
|
||||
assert.equal(suggested[address].symbol, symbol, 'set symbol correctly')
|
||||
assert.equal(suggested[address].decimals, decimals, 'set decimals correctly')
|
||||
assert.equal(suggested[address].image, image, 'set image correctly')
|
||||
})
|
||||
|
||||
it('should add token correctly if user confirms', async function () {
|
||||
const address = '0xabcdef1234567'
|
||||
const symbol = 'ABBR'
|
||||
const decimals = 5
|
||||
const image = 'someimage'
|
||||
req.params.options = { address, symbol, decimals, image }
|
||||
|
||||
sandbox.stub(preferencesController, '_validateERC20AssetParams').returns(true)
|
||||
preferencesController.showWatchAssetUi = async () => {
|
||||
await preferencesController.addToken(address, symbol, decimals, image)
|
||||
}
|
||||
|
||||
await preferencesController._handleWatchAssetERC20(req.params.options)
|
||||
const tokens = preferencesController.getTokens()
|
||||
assert.equal(tokens.length, 1, `one token added`)
|
||||
const added = tokens[0]
|
||||
assert.equal(added.address, address, 'set address correctly')
|
||||
assert.equal(added.symbol, symbol, 'set symbol correctly')
|
||||
assert.equal(added.decimals, decimals, 'set decimals correctly')
|
||||
|
||||
const assetImages = preferencesController.getAssetImages()
|
||||
assert.ok(assetImages[address], `set image correctly`)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
|
@ -227,11 +227,14 @@ var actions = {
|
||||
SET_PROVIDER_TYPE: 'SET_PROVIDER_TYPE',
|
||||
showConfigPage,
|
||||
SHOW_ADD_TOKEN_PAGE: 'SHOW_ADD_TOKEN_PAGE',
|
||||
SHOW_ADD_SUGGESTED_TOKEN_PAGE: 'SHOW_ADD_SUGGESTED_TOKEN_PAGE',
|
||||
showAddTokenPage,
|
||||
showAddSuggestedTokenPage,
|
||||
addToken,
|
||||
addTokens,
|
||||
removeToken,
|
||||
updateTokens,
|
||||
removeSuggestedTokens,
|
||||
UPDATE_TOKENS: 'UPDATE_TOKENS',
|
||||
setRpcTarget: setRpcTarget,
|
||||
setProviderType: setProviderType,
|
||||
@ -1589,11 +1592,18 @@ function showAddTokenPage (transitionForward = true) {
|
||||
}
|
||||
}
|
||||
|
||||
function addToken (address, symbol, decimals) {
|
||||
function showAddSuggestedTokenPage (transitionForward = true) {
|
||||
return {
|
||||
type: actions.SHOW_ADD_SUGGESTED_TOKEN_PAGE,
|
||||
value: transitionForward,
|
||||
}
|
||||
}
|
||||
|
||||
function addToken (address, symbol, decimals, image) {
|
||||
return (dispatch) => {
|
||||
dispatch(actions.showLoadingIndication())
|
||||
return new Promise((resolve, reject) => {
|
||||
background.addToken(address, symbol, decimals, (err, tokens) => {
|
||||
background.addToken(address, symbol, decimals, image, (err, tokens) => {
|
||||
dispatch(actions.hideLoadingIndication())
|
||||
if (err) {
|
||||
dispatch(actions.displayWarning(err.message))
|
||||
@ -1643,6 +1653,27 @@ function addTokens (tokens) {
|
||||
}
|
||||
}
|
||||
|
||||
function removeSuggestedTokens () {
|
||||
return (dispatch) => {
|
||||
dispatch(actions.showLoadingIndication())
|
||||
return new Promise((resolve, reject) => {
|
||||
background.removeSuggestedTokens((err, suggestedTokens) => {
|
||||
dispatch(actions.hideLoadingIndication())
|
||||
if (err) {
|
||||
dispatch(actions.displayWarning(err.message))
|
||||
}
|
||||
dispatch(actions.clearPendingTokens())
|
||||
if (global.METAMASK_UI_TYPE === ENVIRONMENT_TYPE_NOTIFICATION) {
|
||||
return global.platform.closeCurrentWindow()
|
||||
}
|
||||
resolve(suggestedTokens)
|
||||
})
|
||||
})
|
||||
.then(() => updateMetamaskStateFromBackground())
|
||||
.then(suggestedTokens => dispatch(actions.updateMetamaskState({...suggestedTokens})))
|
||||
}
|
||||
}
|
||||
|
||||
function updateTokens (newTokens) {
|
||||
return {
|
||||
type: actions.UPDATE_TOKENS,
|
||||
@ -1650,6 +1681,12 @@ function updateTokens (newTokens) {
|
||||
}
|
||||
}
|
||||
|
||||
function clearPendingTokens () {
|
||||
return {
|
||||
type: actions.CLEAR_PENDING_TOKENS,
|
||||
}
|
||||
}
|
||||
|
||||
function goBackToInitView () {
|
||||
return {
|
||||
type: actions.BACK_TO_INIT_MENU,
|
||||
@ -2310,9 +2347,3 @@ function setPendingTokens (pendingTokens) {
|
||||
payload: tokens,
|
||||
}
|
||||
}
|
||||
|
||||
function clearPendingTokens () {
|
||||
return {
|
||||
type: actions.CLEAR_PENDING_TOKENS,
|
||||
}
|
||||
}
|
||||
|
@ -26,6 +26,7 @@ const RestoreVaultPage = require('./components/pages/keychains/restore-vault').d
|
||||
const RevealSeedConfirmation = require('./components/pages/keychains/reveal-seed')
|
||||
const AddTokenPage = require('./components/pages/add-token')
|
||||
const ConfirmAddTokenPage = require('./components/pages/confirm-add-token')
|
||||
const ConfirmAddSuggestedTokenPage = require('./components/pages/confirm-add-suggested-token')
|
||||
const CreateAccountPage = require('./components/pages/create-account')
|
||||
const NoticeScreen = require('./components/pages/notice')
|
||||
|
||||
@ -51,6 +52,7 @@ const {
|
||||
RESTORE_VAULT_ROUTE,
|
||||
ADD_TOKEN_ROUTE,
|
||||
CONFIRM_ADD_TOKEN_ROUTE,
|
||||
CONFIRM_ADD_SUGGESTED_TOKEN_ROUTE,
|
||||
NEW_ACCOUNT_ROUTE,
|
||||
SEND_ROUTE,
|
||||
CONFIRM_TRANSACTION_ROUTE,
|
||||
@ -85,6 +87,7 @@ class App extends Component {
|
||||
h(Authenticated, { path: SEND_ROUTE, exact, component: SendTransactionScreen }),
|
||||
h(Authenticated, { path: ADD_TOKEN_ROUTE, exact, component: AddTokenPage }),
|
||||
h(Authenticated, { path: CONFIRM_ADD_TOKEN_ROUTE, exact, component: ConfirmAddTokenPage }),
|
||||
h(Authenticated, { path: CONFIRM_ADD_SUGGESTED_TOKEN_ROUTE, exact, component: ConfirmAddSuggestedTokenPage }),
|
||||
h(Authenticated, { path: NEW_ACCOUNT_ROUTE, component: CreateAccountPage }),
|
||||
h(Authenticated, { path: DEFAULT_ROUTE, exact, component: Home }),
|
||||
])
|
||||
|
@ -21,6 +21,7 @@ function mapStateToProps (state) {
|
||||
network,
|
||||
conversionRate: state.metamask.conversionRate,
|
||||
currentCurrency: state.metamask.currentCurrency,
|
||||
assetImages: state.metamask.assetImages,
|
||||
}
|
||||
}
|
||||
|
||||
@ -31,7 +32,9 @@ function BalanceComponent () {
|
||||
|
||||
BalanceComponent.prototype.render = function () {
|
||||
const props = this.props
|
||||
const { token, network } = props
|
||||
const { token, network, assetImages } = props
|
||||
const address = token && token.address
|
||||
const image = assetImages && address ? assetImages[token.address] : undefined
|
||||
|
||||
return h('div.balance-container', {}, [
|
||||
|
||||
@ -42,8 +45,9 @@ BalanceComponent.prototype.render = function () {
|
||||
// }),
|
||||
h(Identicon, {
|
||||
diameter: 50,
|
||||
address: token && token.address,
|
||||
address,
|
||||
network,
|
||||
image,
|
||||
}),
|
||||
|
||||
token ? this.renderTokenBalance() : this.renderBalance(),
|
||||
|
@ -26,37 +26,42 @@ function mapStateToProps (state) {
|
||||
|
||||
IdenticonComponent.prototype.render = function () {
|
||||
var props = this.props
|
||||
const { className = '', address } = props
|
||||
const { className = '', address, image } = props
|
||||
var diameter = props.diameter || this.defaultDiameter
|
||||
|
||||
return address
|
||||
? (
|
||||
h('div', {
|
||||
className: `${className} identicon`,
|
||||
key: 'identicon-' + address,
|
||||
style: {
|
||||
display: 'flex',
|
||||
flexShrink: 0,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
height: diameter,
|
||||
width: diameter,
|
||||
borderRadius: diameter / 2,
|
||||
overflow: 'hidden',
|
||||
},
|
||||
})
|
||||
)
|
||||
: (
|
||||
h('img', {
|
||||
className: `${className} balance-icon`,
|
||||
src: './images/eth_logo.svg',
|
||||
style: {
|
||||
height: diameter,
|
||||
width: diameter,
|
||||
borderRadius: diameter / 2,
|
||||
},
|
||||
})
|
||||
)
|
||||
const style = {
|
||||
height: diameter,
|
||||
width: diameter,
|
||||
borderRadius: diameter / 2,
|
||||
}
|
||||
if (image) {
|
||||
return h('img', {
|
||||
className: `${className} identicon`,
|
||||
src: image,
|
||||
style: {
|
||||
...style,
|
||||
},
|
||||
})
|
||||
} else if (address) {
|
||||
return h('div', {
|
||||
className: `${className} identicon`,
|
||||
key: 'identicon-' + address,
|
||||
style: {
|
||||
display: 'flex',
|
||||
flexShrink: 0,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
...style,
|
||||
overflow: 'hidden',
|
||||
},
|
||||
})
|
||||
} else {
|
||||
return h('img.balance-icon', {
|
||||
src: './images/eth_logo.svg',
|
||||
style: {
|
||||
...style,
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
IdenticonComponent.prototype.componentDidMount = function () {
|
||||
|
@ -10,6 +10,7 @@ function mapStateToProps (state) {
|
||||
return {
|
||||
network: state.metamask.network,
|
||||
token: state.appState.modal.modalState.props.token,
|
||||
assetImages: state.metamask.assetImages,
|
||||
}
|
||||
}
|
||||
|
||||
@ -40,8 +41,9 @@ module.exports = connect(mapStateToProps, mapDispatchToProps)(HideTokenConfirmat
|
||||
|
||||
|
||||
HideTokenConfirmationModal.prototype.render = function () {
|
||||
const { token, network, hideToken, hideModal } = this.props
|
||||
const { token, network, hideToken, hideModal, assetImages } = this.props
|
||||
const { symbol, address } = token
|
||||
const image = assetImages[address]
|
||||
|
||||
return h('div.hide-token-confirmation', {}, [
|
||||
h('div.hide-token-confirmation__container', {
|
||||
@ -55,6 +57,7 @@ HideTokenConfirmationModal.prototype.render = function () {
|
||||
diameter: 45,
|
||||
address,
|
||||
network,
|
||||
image,
|
||||
}),
|
||||
|
||||
h('div.hide-token-confirmation__symbol', {}, symbol),
|
||||
|
@ -0,0 +1,126 @@
|
||||
import React, { Component } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import { DEFAULT_ROUTE } from '../../../routes'
|
||||
import Button from '../../button'
|
||||
import Identicon from '../../../components/identicon'
|
||||
import TokenBalance from '../../token-balance'
|
||||
|
||||
export default class ConfirmAddSuggestedToken extends Component {
|
||||
static contextTypes = {
|
||||
t: PropTypes.func,
|
||||
}
|
||||
|
||||
static propTypes = {
|
||||
history: PropTypes.object,
|
||||
clearPendingTokens: PropTypes.func,
|
||||
addToken: PropTypes.func,
|
||||
pendingTokens: PropTypes.object,
|
||||
removeSuggestedTokens: PropTypes.func,
|
||||
}
|
||||
|
||||
componentDidMount () {
|
||||
const { pendingTokens = {}, history } = this.props
|
||||
|
||||
if (Object.keys(pendingTokens).length === 0) {
|
||||
history.push(DEFAULT_ROUTE)
|
||||
}
|
||||
}
|
||||
|
||||
getTokenName (name, symbol) {
|
||||
return typeof name === 'undefined'
|
||||
? symbol
|
||||
: `${name} (${symbol})`
|
||||
}
|
||||
|
||||
render () {
|
||||
const { addToken, pendingTokens, removeSuggestedTokens, history } = this.props
|
||||
const pendingTokenKey = Object.keys(pendingTokens)[0]
|
||||
const pendingToken = pendingTokens[pendingTokenKey]
|
||||
|
||||
return (
|
||||
<div className="page-container">
|
||||
<div className="page-container__header">
|
||||
<div className="page-container__title">
|
||||
{ this.context.t('addSuggestedTokens') }
|
||||
</div>
|
||||
<div className="page-container__subtitle">
|
||||
{ this.context.t('likeToAddTokens') }
|
||||
</div>
|
||||
</div>
|
||||
<div className="page-container__content">
|
||||
<div className="confirm-add-token">
|
||||
<div className="confirm-add-token__header">
|
||||
<div className="confirm-add-token__token">
|
||||
{ this.context.t('token') }
|
||||
</div>
|
||||
<div className="confirm-add-token__balance">
|
||||
{ this.context.t('balance') }
|
||||
</div>
|
||||
</div>
|
||||
<div className="confirm-add-token__token-list">
|
||||
{
|
||||
Object.entries(pendingTokens)
|
||||
.map(([ address, token ]) => {
|
||||
const { name, symbol, image } = token
|
||||
|
||||
return (
|
||||
<div
|
||||
className="confirm-add-token__token-list-item"
|
||||
key={address}
|
||||
>
|
||||
<div className="confirm-add-token__token confirm-add-token__data">
|
||||
<Identicon
|
||||
className="confirm-add-token__token-icon"
|
||||
diameter={48}
|
||||
address={address}
|
||||
image={image}
|
||||
/>
|
||||
<div className="confirm-add-token__name">
|
||||
{ this.getTokenName(name, symbol) }
|
||||
</div>
|
||||
</div>
|
||||
<div className="confirm-add-token__balance">
|
||||
<TokenBalance token={token} />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="page-container__footer">
|
||||
<Button
|
||||
type="default"
|
||||
large
|
||||
className="page-container__footer-button"
|
||||
onClick={() => {
|
||||
removeSuggestedTokens()
|
||||
.then(() => {
|
||||
history.push(DEFAULT_ROUTE)
|
||||
})
|
||||
}}
|
||||
>
|
||||
{ this.context.t('cancel') }
|
||||
</Button>
|
||||
<Button
|
||||
type="primary"
|
||||
large
|
||||
className="page-container__footer-button"
|
||||
onClick={() => {
|
||||
addToken(pendingToken)
|
||||
.then(() => {
|
||||
removeSuggestedTokens()
|
||||
.then(() => {
|
||||
history.push(DEFAULT_ROUTE)
|
||||
})
|
||||
})
|
||||
}}
|
||||
>
|
||||
{ this.context.t('addToken') }
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
@ -0,0 +1,29 @@
|
||||
import { connect } from 'react-redux'
|
||||
import { compose } from 'recompose'
|
||||
import ConfirmAddSuggestedToken from './confirm-add-suggested-token.component'
|
||||
import { withRouter } from 'react-router-dom'
|
||||
|
||||
const extend = require('xtend')
|
||||
|
||||
const { addToken, removeSuggestedTokens } = require('../../../actions')
|
||||
|
||||
const mapStateToProps = ({ metamask }) => {
|
||||
const { pendingTokens, suggestedTokens } = metamask
|
||||
const params = extend(pendingTokens, suggestedTokens)
|
||||
|
||||
return {
|
||||
pendingTokens: params,
|
||||
}
|
||||
}
|
||||
|
||||
const mapDispatchToProps = dispatch => {
|
||||
return {
|
||||
addToken: ({address, symbol, decimals, image}) => dispatch(addToken(address, symbol, decimals, image)),
|
||||
removeSuggestedTokens: () => dispatch(removeSuggestedTokens()),
|
||||
}
|
||||
}
|
||||
|
||||
export default compose(
|
||||
withRouter,
|
||||
connect(mapStateToProps, mapDispatchToProps)
|
||||
)(ConfirmAddSuggestedToken)
|
@ -0,0 +1,2 @@
|
||||
import ConfirmAddSuggestedToken from './confirm-add-suggested-token.container'
|
||||
module.exports = ConfirmAddSuggestedToken
|
@ -9,6 +9,7 @@ import {
|
||||
RESTORE_VAULT_ROUTE,
|
||||
CONFIRM_TRANSACTION_ROUTE,
|
||||
NOTICE_ROUTE,
|
||||
CONFIRM_ADD_SUGGESTED_TOKEN_ROUTE,
|
||||
} from '../../../routes'
|
||||
|
||||
export default class Home extends PureComponent {
|
||||
@ -18,11 +19,21 @@ export default class Home extends PureComponent {
|
||||
lostAccounts: PropTypes.array,
|
||||
forgottenPassword: PropTypes.bool,
|
||||
seedWords: PropTypes.string,
|
||||
suggestedTokens: PropTypes.object,
|
||||
unconfirmedTransactionsCount: PropTypes.number,
|
||||
}
|
||||
|
||||
componentDidMount () {
|
||||
const { history, unconfirmedTransactionsCount = 0 } = this.props
|
||||
const {
|
||||
history,
|
||||
suggestedTokens = {},
|
||||
unconfirmedTransactionsCount = 0,
|
||||
} = this.props
|
||||
|
||||
// suggested new tokens
|
||||
if (Object.keys(suggestedTokens).length > 0) {
|
||||
history.push(CONFIRM_ADD_SUGGESTED_TOKEN_ROUTE)
|
||||
}
|
||||
|
||||
if (unconfirmedTransactionsCount > 0) {
|
||||
history.push(CONFIRM_TRANSACTION_ROUTE)
|
||||
|
@ -10,6 +10,7 @@ const mapStateToProps = state => {
|
||||
noActiveNotices,
|
||||
lostAccounts,
|
||||
seedWords,
|
||||
suggestedTokens,
|
||||
} = metamask
|
||||
const { forgottenPassword } = appState
|
||||
|
||||
@ -18,6 +19,7 @@ const mapStateToProps = state => {
|
||||
lostAccounts,
|
||||
forgottenPassword,
|
||||
seedWords,
|
||||
suggestedTokens,
|
||||
unconfirmedTransactionsCount: unconfirmedTransactionsCountSelector(state),
|
||||
}
|
||||
}
|
||||
|
@ -56,8 +56,8 @@ TokenCell.prototype.render = function () {
|
||||
sidebarOpen,
|
||||
currentCurrency,
|
||||
// userAddress,
|
||||
image,
|
||||
} = props
|
||||
|
||||
let currentTokenToFiatRate
|
||||
let currentTokenInFiat
|
||||
let formattedFiat = ''
|
||||
@ -97,6 +97,7 @@ TokenCell.prototype.render = function () {
|
||||
diameter: 50,
|
||||
address,
|
||||
network,
|
||||
image,
|
||||
}),
|
||||
|
||||
h('div.token-list-item__balance-ellipsis', null, [
|
||||
|
@ -13,6 +13,7 @@ function mapStateToProps (state) {
|
||||
network: state.metamask.network,
|
||||
tokens: state.metamask.tokens,
|
||||
userAddress: selectors.getSelectedAddress(state),
|
||||
assetImages: state.metamask.assetImages,
|
||||
}
|
||||
}
|
||||
|
||||
@ -44,10 +45,9 @@ function TokenList () {
|
||||
}
|
||||
|
||||
TokenList.prototype.render = function () {
|
||||
const { userAddress } = this.props
|
||||
const { userAddress, assetImages } = this.props
|
||||
const state = this.state
|
||||
const { tokens, isLoading, error } = state
|
||||
|
||||
if (isLoading) {
|
||||
return this.message(this.context.t('loadingTokens'))
|
||||
}
|
||||
@ -74,7 +74,10 @@ TokenList.prototype.render = function () {
|
||||
])
|
||||
}
|
||||
|
||||
return h('div', tokens.map((tokenData) => h(TokenCell, tokenData)))
|
||||
return h('div', tokens.map((tokenData) => {
|
||||
tokenData.image = assetImages[tokenData.address]
|
||||
return h(TokenCell, tokenData)
|
||||
}))
|
||||
|
||||
}
|
||||
|
||||
|
@ -209,6 +209,15 @@ function reduceApp (state, action) {
|
||||
transForward: action.value,
|
||||
})
|
||||
|
||||
case actions.SHOW_ADD_SUGGESTED_TOKEN_PAGE:
|
||||
return extend(appState, {
|
||||
currentView: {
|
||||
name: 'add-suggested-token',
|
||||
context: appState.currentView.context,
|
||||
},
|
||||
transForward: action.value,
|
||||
})
|
||||
|
||||
case actions.SHOW_IMPORT_PAGE:
|
||||
return extend(appState, {
|
||||
currentView: {
|
||||
|
@ -7,6 +7,7 @@ const CONFIRM_SEED_ROUTE = '/confirm-seed'
|
||||
const RESTORE_VAULT_ROUTE = '/restore-vault'
|
||||
const ADD_TOKEN_ROUTE = '/add-token'
|
||||
const CONFIRM_ADD_TOKEN_ROUTE = '/confirm-add-token'
|
||||
const CONFIRM_ADD_SUGGESTED_TOKEN_ROUTE = '/confirm-add-suggested-token'
|
||||
const NEW_ACCOUNT_ROUTE = '/new-account'
|
||||
const IMPORT_ACCOUNT_ROUTE = '/new-account/import'
|
||||
const CONNECT_HARDWARE_ROUTE = '/new-account/connect'
|
||||
@ -41,6 +42,7 @@ module.exports = {
|
||||
RESTORE_VAULT_ROUTE,
|
||||
ADD_TOKEN_ROUTE,
|
||||
CONFIRM_ADD_TOKEN_ROUTE,
|
||||
CONFIRM_ADD_SUGGESTED_TOKEN_ROUTE,
|
||||
NEW_ACCOUNT_ROUTE,
|
||||
IMPORT_ACCOUNT_ROUTE,
|
||||
CONNECT_HARDWARE_ROUTE,
|
||||
|
Loading…
Reference in New Issue
Block a user