1
0
mirror of https://github.com/kremalicious/metamask-extension.git synced 2024-12-23 09:52:26 +01:00

Use method middleware for watchAsset (#9943)

* Use method middleware for watchAsset
* Update validation error messages
* Make addSuggestedERC20Asset private
* Remove redundant check in _handleWatchAssetERC20
This commit is contained in:
Erik Marks 2020-12-02 08:49:49 -08:00 committed by GitHub
parent 9d4b8a4903
commit c3eb272af9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 127 additions and 119 deletions

View File

@ -1,12 +1,12 @@
import { strict as assert } from 'assert' import { strict as assert } from 'assert'
import ObservableStore from 'obs-store' import ObservableStore from 'obs-store'
import { ethErrors } from 'eth-json-rpc-errors'
import { normalize as normalizeAddress } from 'eth-sig-util' import { normalize as normalizeAddress } from 'eth-sig-util'
import { isValidAddress, sha3, bufferToHex } from 'ethereumjs-util' import { isValidAddress, sha3, bufferToHex } from 'ethereumjs-util'
import ethers from 'ethers' import ethers from 'ethers'
import log from 'loglevel' import log from 'loglevel'
import { isPrefixedFormattedHexString } from '../lib/util' import { isPrefixedFormattedHexString } from '../lib/util'
import { LISTED_CONTRACT_ADDRESSES } from '../../../shared/constants/tokens' import { LISTED_CONTRACT_ADDRESSES } from '../../../shared/constants/tokens'
import { addInternalMethodPrefix } from './permissions'
import { NETWORK_TYPE_TO_ID_MAP } from './network/enums' import { NETWORK_TYPE_TO_ID_MAP } from './network/enums'
export default class PreferencesController { export default class PreferencesController {
@ -171,22 +171,6 @@ export default class PreferencesController {
return this.store.getState().assetImages 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,
unlisted: !LISTED_CONTRACT_ADDRESSES.includes(address.toLowerCase()),
}
suggested[address] = newEntry
this.store.updateState({ suggestedTokens: suggested })
}
/** /**
* Add new methodData to state, to avoid requesting this information again through Infura * Add new methodData to state, to avoid requesting this information again through Infura
* *
@ -200,37 +184,21 @@ export default class PreferencesController {
} }
/** /**
* RPC engine middleware for requesting new asset added * wallet_watchAsset request handler.
* *
* @param {any} req * @param {Object} req - The watchAsset JSON-RPC request object.
* @param {any} res
* @param {Function} next
* @param {Function} end
*/ */
async requestWatchAsset(req, res, next, end) { async requestWatchAsset(req) {
if ( const { type, options } = req.params
req.method === 'metamask_watchAsset' ||
req.method === addInternalMethodPrefix('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()
}
return
}
default:
end(new Error(`Asset of type ${type} not supported`))
return
}
}
next() switch (type) {
case 'ERC20':
return await this._handleWatchAssetERC20(options)
default:
throw ethErrors.rpc.invalidParams(
`Asset of type "${type}" not supported.`,
)
}
} }
/** /**
@ -775,21 +743,17 @@ export default class PreferencesController {
* *
*/ */
async _handleWatchAssetERC20(tokenMetadata) { async _handleWatchAssetERC20(tokenMetadata) {
const { address, symbol, decimals, image } = tokenMetadata this._validateERC20AssetParams(tokenMetadata)
const rawAddress = address
try { const address = normalizeAddress(tokenMetadata.address)
this._validateERC20AssetParams({ rawAddress, symbol, decimals }) const { symbol, decimals, image } = tokenMetadata
} catch (err) { this._addSuggestedERC20Asset(address, symbol, decimals, image)
return err
} await this.openPopup()
const tokenOpts = { rawAddress, decimals, symbol, image } const tokenAddresses = this.getTokens().filter(
this.addSuggestedERC20Asset(tokenOpts) (token) => token.address === address,
return this.openPopup().then(() => { )
const tokenAddresses = this.getTokens().filter( return tokenAddresses.length > 0
(token) => token.address === normalizeAddress(rawAddress),
)
return tokenAddresses.length > 0
})
} }
/** /**
@ -800,26 +764,41 @@ export default class PreferencesController {
* doesn't fulfill requirements * doesn't fulfill requirements
* *
*/ */
_validateERC20AssetParams({ rawAddress, symbol, decimals } = {}) { _validateERC20AssetParams({ address, symbol, decimals }) {
if (!rawAddress || !symbol || typeof decimals === 'undefined') { if (!address || !symbol || typeof decimals === 'undefined') {
throw new Error( throw ethErrors.rpc.invalidParams(
`Cannot suggest token without address, symbol, and decimals`, `Must specify address, symbol, and decimals.`,
) )
} }
if (typeof symbol !== 'string') { if (typeof symbol !== 'string') {
throw new Error(`Invalid symbol: not a string`) throw ethErrors.rpc.invalidParams(`Invalid symbol: not a string.`)
} }
if (symbol.length > 6) { if (!(symbol.length < 7)) {
throw new Error(`Invalid symbol ${symbol} more than six characters`) throw ethErrors.rpc.invalidParams(
`Invalid symbol "${symbol}": longer than 6 characters.`,
)
} }
const numDecimals = parseInt(decimals, 10) const numDecimals = parseInt(decimals, 10)
if (isNaN(numDecimals) || numDecimals > 36 || numDecimals < 0) { if (isNaN(numDecimals) || numDecimals > 36 || numDecimals < 0) {
throw new Error( throw ethErrors.rpc.invalidParams(
`Invalid decimals ${decimals} must be at least 0, and not over 36`, `Invalid decimals "${decimals}": must be 0 <= 36.`,
) )
} }
if (!isValidAddress(rawAddress)) { if (!isValidAddress(address)) {
throw new Error(`Invalid address ${rawAddress}`) throw ethErrors.rpc.invalidParams(`Invalid address "${address}".`)
} }
} }
_addSuggestedERC20Asset(address, symbol, decimals, image) {
const newEntry = {
address,
symbol,
decimals,
image,
unlisted: !LISTED_CONTRACT_ADDRESSES.includes(address),
}
const suggested = this.getSuggestedTokens()
suggested[address] = newEntry
this.store.updateState({ suggestedTokens: suggested })
}
} }

View File

@ -25,6 +25,8 @@ const MESSAGE_TYPE = {
ETH_SIGN_TYPED_DATA: 'eth_signTypedData', ETH_SIGN_TYPED_DATA: 'eth_signTypedData',
LOG_WEB3_USAGE: 'metamask_logInjectedWeb3Usage', LOG_WEB3_USAGE: 'metamask_logInjectedWeb3Usage',
PERSONAL_SIGN: 'personal_sign', PERSONAL_SIGN: 'personal_sign',
WATCH_ASSET: 'wallet_watchAsset',
WATCH_ASSET_LEGACY: 'metamask_watchAsset',
} }
export { export {

View File

@ -1,7 +1,9 @@
import handlers from './handlers' import handlers from './handlers'
const handlerMap = handlers.reduce((map, handler) => { const handlerMap = handlers.reduce((map, handler) => {
map.set(handler.methodName, handler.implementation) for (const methodName of handler.methodNames) {
map.set(methodName, handler.implementation)
}
return map return map
}, new Map()) }, new Map())

View File

@ -1,4 +1,5 @@
import logWeb3Usage from './log-web3-usage' import logWeb3Usage from './log-web3-usage'
import watchAsset from './watch-asset'
const handlers = [logWeb3Usage] const handlers = [logWeb3Usage, watchAsset]
export default handlers export default handlers

View File

@ -8,7 +8,7 @@ import { MESSAGE_TYPE } from '../../enums'
*/ */
const logWeb3Usage = { const logWeb3Usage = {
methodName: MESSAGE_TYPE.LOG_WEB3_USAGE, methodNames: [MESSAGE_TYPE.LOG_WEB3_USAGE],
implementation: logWeb3UsageHandler, implementation: logWeb3UsageHandler,
} }
export default logWeb3Usage export default logWeb3Usage

View File

@ -0,0 +1,40 @@
import { MESSAGE_TYPE } from '../../enums'
const watchAsset = {
methodNames: [MESSAGE_TYPE.WATCH_ASSET, MESSAGE_TYPE.WATCH_ASSET_LEGACY],
implementation: watchAssetHandler,
}
export default watchAsset
/**
* @typedef {Object} WatchAssetOptions
* @property {Function} handleWatchAssetRequest - The wallet_watchAsset method implementation.
*/
/**
* @typedef {Object} WatchAssetParam
* @property {string} type - The type of the asset to watch.
* @property {Object} options - Watch options for the asset.
*/
/**
* @param {import('json-rpc-engine').JsonRpcRequest<WatchAssetParam>} req - The JSON-RPC request object.
* @param {import('json-rpc-engine').JsonRpcResponse<true>} res - The JSON-RPC response object.
* @param {Function} _next - The json-rpc-engine 'next' callback.
* @param {Function} end - The json-rpc-engine 'end' callback.
* @param {WatchAssetOptions} options
*/
async function watchAssetHandler(
req,
res,
_next,
end,
{ handleWatchAssetRequest },
) {
try {
res.result = await handleWatchAssetRequest(req)
return end()
} catch (error) {
return end(error)
}
}

View File

@ -1968,6 +1968,9 @@ export default class MetamaskController extends EventEmitter {
createMethodMiddleware({ createMethodMiddleware({
origin, origin,
sendMetrics: this.trackMetaMetricsEvent, sendMetrics: this.trackMetaMetricsEvent,
handleWatchAssetRequest: this.preferencesController.requestWatchAsset.bind(
this.preferencesController,
),
}), }),
) )
// filter and subscription polyfills // filter and subscription polyfills
@ -1979,12 +1982,6 @@ export default class MetamaskController extends EventEmitter {
this.permissionsController.createMiddleware({ origin, extensionId }), this.permissionsController.createMiddleware({ origin, extensionId }),
) )
} }
// watch asset
engine.push(
this.preferencesController.requestWatchAsset.bind(
this.preferencesController,
),
)
// forward to metamask primary provider // forward to metamask primary provider
engine.push(providerAsMiddleware(provider)) engine.push(providerAsMiddleware(provider))
return engine return engine

View File

@ -2,7 +2,6 @@ import assert from 'assert'
import ObservableStore from 'obs-store' import ObservableStore from 'obs-store'
import sinon from 'sinon' import sinon from 'sinon'
import PreferencesController from '../../../../app/scripts/controllers/preferences' import PreferencesController from '../../../../app/scripts/controllers/preferences'
import { addInternalMethodPrefix } from '../../../../app/scripts/controllers/permissions'
describe('preferences controller', function () { describe('preferences controller', function () {
let preferencesController let preferencesController
@ -405,53 +404,41 @@ describe('preferences controller', function () {
}) })
describe('on watchAsset', function () { describe('on watchAsset', function () {
let stubHandleWatchAssetERC20, asy, req, res let req, stubHandleWatchAssetERC20
const sandbox = sinon.createSandbox() const sandbox = sinon.createSandbox()
beforeEach(function () { beforeEach(function () {
req = { params: {} } req = { method: 'wallet_watchAsset', params: {} }
res = {}
asy = { next: sandbox.spy(), end: sandbox.spy() }
stubHandleWatchAssetERC20 = sandbox.stub( stubHandleWatchAssetERC20 = sandbox.stub(
preferencesController, preferencesController,
'_handleWatchAssetERC20', '_handleWatchAssetERC20',
) )
}) })
after(function () { after(function () {
sandbox.restore() sandbox.restore()
}) })
it('shouldn not do anything if method not corresponds', async function () { it('should error if passed no type', async function () {
req.method = 'metamask' await assert.rejects(
await preferencesController.requestWatchAsset(req, res, asy.next, asy.end) () => preferencesController.requestWatchAsset(req),
sandbox.assert.notCalled(asy.end) { message: 'Asset of type "undefined" not supported.' },
sandbox.assert.called(asy.next) 'should have errored',
)
}) })
it('should do something if method is supported', async function () {
req.method = 'metamask_watchAsset' it('should error if method is not supported', async function () {
req.params.type = 'someasset' req.params.type = 'someasset'
await preferencesController.requestWatchAsset(req, res, asy.next, asy.end) await assert.rejects(
sandbox.assert.called(asy.end) () => preferencesController.requestWatchAsset(req),
sandbox.assert.notCalled(asy.next) { message: 'Asset of type "someasset" not supported.' },
req.method = addInternalMethodPrefix('watchAsset') 'should have errored',
req.params.type = 'someasset' )
await preferencesController.requestWatchAsset(req, res, asy.next, asy.end)
sandbox.assert.calledTwice(asy.end)
sandbox.assert.notCalled(asy.next)
}) })
it('should through error if method is supported but asset type is not', async function () {
req.method = 'metamask_watchAsset' it('should handle ERC20 type', async function () {
req.params.type = 'someasset'
await preferencesController.requestWatchAsset(req, res, asy.next, asy.end)
sandbox.assert.called(asy.end)
sandbox.assert.notCalled(stubHandleWatchAssetERC20)
sandbox.assert.notCalled(asy.next)
assert.deepEqual(res, {})
})
it('should trigger handle add asset if type supported', async function () {
req.method = 'metamask_watchAsset'
req.params.type = 'ERC20' req.params.type = 'ERC20'
await preferencesController.requestWatchAsset(req, res, asy.next, asy.end) await preferencesController.requestWatchAsset(req)
sandbox.assert.called(stubHandleWatchAssetERC20) sandbox.assert.called(stubHandleWatchAssetERC20)
}) })
}) })
@ -527,7 +514,7 @@ describe('preferences controller', function () {
assert.doesNotThrow(() => assert.doesNotThrow(() =>
validate({ validate({
rawAddress: '0xd26114cd6EE289AccF82350c8d8487fedB8A0C07', address: '0xd26114cd6EE289AccF82350c8d8487fedB8A0C07',
symbol: 'ABC', symbol: 'ABC',
decimals: 0, decimals: 0,
}), }),
@ -539,7 +526,7 @@ describe('preferences controller', function () {
assert.throws( assert.throws(
() => () =>
validate({ validate({
rawAddress: '0xd26114cd6EE289AccF82350c8d8487fedB8A0C07', address: '0xd26114cd6EE289AccF82350c8d8487fedB8A0C07',
decimals: 0, decimals: 0,
}), }),
'missing symbol should fail', 'missing symbol should fail',
@ -547,7 +534,7 @@ describe('preferences controller', function () {
assert.throws( assert.throws(
() => () =>
validate({ validate({
rawAddress: '0xd26114cd6EE289AccF82350c8d8487fedB8A0C07', address: '0xd26114cd6EE289AccF82350c8d8487fedB8A0C07',
symbol: 'ABC', symbol: 'ABC',
}), }),
'missing decimals should fail', 'missing decimals should fail',
@ -555,7 +542,7 @@ describe('preferences controller', function () {
assert.throws( assert.throws(
() => () =>
validate({ validate({
rawAddress: '0xd26114cd6EE289AccF82350c8d8487fedB8A0C07', address: '0xd26114cd6EE289AccF82350c8d8487fedB8A0C07',
symbol: 'ABCDEFGHI', symbol: 'ABCDEFGHI',
decimals: 0, decimals: 0,
}), }),
@ -564,7 +551,7 @@ describe('preferences controller', function () {
assert.throws( assert.throws(
() => () =>
validate({ validate({
rawAddress: '0xd26114cd6EE289AccF82350c8d8487fedB8A0C07', address: '0xd26114cd6EE289AccF82350c8d8487fedB8A0C07',
symbol: 'ABC', symbol: 'ABC',
decimals: -1, decimals: -1,
}), }),
@ -573,14 +560,14 @@ describe('preferences controller', function () {
assert.throws( assert.throws(
() => () =>
validate({ validate({
rawAddress: '0xd26114cd6EE289AccF82350c8d8487fedB8A0C07', address: '0xd26114cd6EE289AccF82350c8d8487fedB8A0C07',
symbol: 'ABC', symbol: 'ABC',
decimals: 38, decimals: 38,
}), }),
'decimals > 36 should fail', 'decimals > 36 should fail',
) )
assert.throws( assert.throws(
() => validate({ rawAddress: '0x123', symbol: 'ABC', decimals: 0 }), () => validate({ address: '0x123', symbol: 'ABC', decimals: 0 }),
'invalid address should fail', 'invalid address should fail',
) )
}) })