Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add metamask_watchAsset #4606

Merged
merged 41 commits into from
Aug 28, 2018
Merged
Show file tree
Hide file tree
Changes from 40 commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
5d7c281
Begin adding eth_watchToken
danfinlay Jun 18, 2018
f14ed32
Begin letting UI show suggested tokens
danfinlay Jun 18, 2018
5e4f3e4
Get popup appearing when suggesting new token
danfinlay Jun 18, 2018
d26ce80
Linted
danfinlay Jun 18, 2018
0481335
Improved rpc-engine usage
danfinlay Jun 18, 2018
21a61f2
merge develop
estebanmino Aug 3, 2018
081884b
rpc-engine not crashing when eth_watchToken
estebanmino Aug 3, 2018
12dd7a7
popup initializing with suggested tokens
estebanmino Aug 3, 2018
9ac9f53
eth_watchToken working
estebanmino Aug 3, 2018
78ad3c3
add suggested token params validation
estebanmino Aug 6, 2018
88933f3
fix duplicated action
estebanmino Aug 6, 2018
af35b41
new confirm add suggested token component
estebanmino Aug 7, 2018
a57f56f
clean confirm add token component
estebanmino Aug 7, 2018
1f8a808
wip watch token old ui
estebanmino Aug 7, 2018
00d1f6f
watch token on old ui
estebanmino Aug 7, 2018
3f57d5f
Merge branch 'develop' into WatchTokenFeature
estebanmino Aug 7, 2018
15ea8c0
fix merge
estebanmino Aug 7, 2018
33357e3
refactor unused code
estebanmino Aug 7, 2018
c245405
show token address summary on old ui
estebanmino Aug 8, 2018
8f5b80a
update method to metamask_watchToken
estebanmino Aug 14, 2018
a4c3f6b
add support for images base64 and urls on new ui
estebanmino Aug 14, 2018
a4b6b23
watchToken to watchAsset
estebanmino Aug 14, 2018
b766104
add suggested tokens objects in metamask state
estebanmino Aug 15, 2018
5289a36
change watchAsset to new spec for type ERC20
estebanmino Aug 15, 2018
a36ea0e
show watch asset image from hide token modal
estebanmino Aug 16, 2018
2ace30b
WIP
estebanmino Aug 16, 2018
bb868f5
correct behavior when notification is closed when popup
estebanmino Aug 16, 2018
dbab9a0
delete according image when token added with watchToken deleted
estebanmino Aug 17, 2018
81cd29d
Merge branch 'develop' into WatchTokenFeature
estebanmino Aug 20, 2018
68c1b4c
watchAsset returns result wether token was added or not
estebanmino Aug 21, 2018
6fa889a
refactor watchToken related functions
estebanmino Aug 21, 2018
3a3732e
returning error in watchAsset
estebanmino Aug 21, 2018
6ccf281
unit tests for watchAsset
estebanmino Aug 21, 2018
4e6c71e
Merge branch 'develop' into WatchTokenFeature
estebanmino Aug 21, 2018
153731e
fix integration tests on balance component with new watchAsset
estebanmino Aug 22, 2018
56bed3f
expose web3.metamask.watchAsset
estebanmino Aug 22, 2018
b59a1e9
typo watchAsset imageUrl to image
estebanmino Aug 23, 2018
053e262
delete web3.metamassk.watchAsset
estebanmino Aug 23, 2018
8af45d5
add watchAsset CHANGELOG
estebanmino Aug 23, 2018
3106374
watchAsset small changes
estebanmino Aug 28, 2018
e743f44
fix conflicts
estebanmino Aug 28, 2018
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
3 changes: 3 additions & 0 deletions app/_locales/en/messages.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,9 @@
"addTokens": {
"message": "Add Tokens"
},
"addSuggestedTokens": {
"message": "Add Suggested Tokens"
},
"addAcquiredTokens": {
"message": "Add the tokens you've acquired using MetaMask"
},
Expand Down
20 changes: 20 additions & 0 deletions app/scripts/background.js
Original file line number Diff line number Diff line change
Expand Up @@ -256,6 +256,7 @@ function setupController (initState, initLangCode) {
showUnconfirmedMessage: triggerUi,
unlockAccountMessage: triggerUi,
showUnapprovedTx: triggerUi,
showWatchAssetUi: showWatchAssetUi,
// initial state
initState,
// initial locale code
Expand Down Expand Up @@ -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'})
}
})

116 changes: 110 additions & 6 deletions app/scripts/controllers/preferences.js
Original file line number Diff line number Diff line change
@@ -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')


Expand All @@ -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
Expand All @@ -26,7 +28,9 @@ class PreferencesController {
frequentRpcList: [],
currentAccountTab: 'history',
accountTokens: {},
assetImages: {},
tokens: [],
suggestedTokens: {},
useBlockie: false,
featureFlags: {},
currentLocale: opts.initLangCode,
Expand All @@ -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
Expand All @@ -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
*
Expand Down Expand Up @@ -186,6 +238,13 @@ class PreferencesController {
return selected
}

removeSuggestedTokens () {
return new Promise((resolve, reject) => {
this.store.updateState({ suggestedTokens: {} })
resolve({})
})
}

/**
* Setter for the `selectedAddress` property
*
Expand Down Expand Up @@ -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
})
Expand All @@ -247,7 +306,8 @@ class PreferencesController {
} else {
tokens.push(newEntry)
}
this._updateAccountTokens(tokens)
assetImages[address] = image
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure if it's really needed but maybe use toChecksumAddress or normalizeAddress to make sure it will always find the right key (assuming that assetImages keys are already checksummed)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, in line 295 the normalization happens, so for each token added it will have as key the correct normalized address.

this._updateAccountTokens(tokens, assetImages)
return Promise.resolve(tokens)
}

Expand All @@ -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)
}

Expand Down Expand Up @@ -387,6 +449,7 @@ class PreferencesController {
//
// PRIVATE METHODS
//

/**
* Subscription to network provider type.
*
Expand All @@ -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 })
}

/**
Expand Down Expand Up @@ -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
3 changes: 3 additions & 0 deletions app/scripts/metamask-controller.js
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ module.exports = class MetamaskController extends EventEmitter {
this.preferencesController = new PreferencesController({
initState: initState.PreferencesController,
initLangCode: opts.initLangCode,
showWatchAssetUi: opts.showWatchAssetUi,
network: this.networkController,
})

Expand Down Expand Up @@ -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),
Expand Down Expand Up @@ -1242,6 +1244,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
Expand Down
5 changes: 5 additions & 0 deletions old-ui/app/account-detail.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
}
}
Expand All @@ -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', [
Expand Down
Loading