Merge branch 'network-remove-provider-engine' of https://github.com/MetaMask/metamask-extension into transactions-use-new-block-tracker
commit
61caee9d94
After Width: | Height: | Size: 1.2 KiB |
After Width: | Height: | Size: 5.5 KiB |
@ -0,0 +1,66 @@ |
||||
const log = require('loglevel') |
||||
|
||||
/** |
||||
* JSON-RPC error object |
||||
* |
||||
* @typedef {Object} RpcError |
||||
* @property {number} code - Indicates the error type that occurred |
||||
* @property {Object} [data] - Contains additional information about the error |
||||
* @property {string} [message] - Short description of the error |
||||
*/ |
||||
|
||||
/** |
||||
* Middleware configuration object |
||||
* |
||||
* @typedef {Object} MiddlewareConfig |
||||
* @property {boolean} [override] - Use RPC_ERRORS message in place of provider message |
||||
*/ |
||||
|
||||
/** |
||||
* Map of standard and non-standard RPC error codes to messages |
||||
*/ |
||||
const RPC_ERRORS = { |
||||
1: 'An unauthorized action was attempted.', |
||||
2: 'A disallowed action was attempted.', |
||||
3: 'An execution error occurred.', |
||||
[-32600]: 'The JSON sent is not a valid Request object.', |
||||
[-32601]: 'The method does not exist / is not available.', |
||||
[-32602]: 'Invalid method parameter(s).', |
||||
[-32603]: 'Internal JSON-RPC error.', |
||||
[-32700]: 'Invalid JSON was received by the server. An error occurred on the server while parsing the JSON text.', |
||||
internal: 'Internal server error.', |
||||
unknown: 'Unknown JSON-RPC error.', |
||||
} |
||||
|
||||
/** |
||||
* Modifies a JSON-RPC error object in-place to add a human-readable message, |
||||
* optionally overriding any provider-supplied message |
||||
* |
||||
* @param {RpcError} error - JSON-RPC error object |
||||
* @param {boolean} override - Use RPC_ERRORS message in place of provider message |
||||
*/ |
||||
function sanitizeRPCError (error, override) { |
||||
if (error.message && !override) { return error } |
||||
const message = error.code > -31099 && error.code < -32100 ? RPC_ERRORS.internal : RPC_ERRORS[error.code] |
||||
error.message = message || RPC_ERRORS.unknown |
||||
} |
||||
|
||||
/** |
||||
* json-rpc-engine middleware that both logs standard and non-standard error |
||||
* messages and ends middleware stack traversal if an error is encountered |
||||
* |
||||
* @param {MiddlewareConfig} [config={override:true}] - Middleware configuration |
||||
* @returns {Function} json-rpc-engine middleware function |
||||
*/ |
||||
function createErrorMiddleware ({ override = true } = {}) { |
||||
return (req, res, next) => { |
||||
next(done => { |
||||
const { error } = res |
||||
if (!error) { return done() } |
||||
sanitizeRPCError(error) |
||||
log.error(`MetaMask - RPC Error: ${error.message}`, error) |
||||
}) |
||||
} |
||||
} |
||||
|
||||
module.exports = createErrorMiddleware |
@ -0,0 +1,47 @@ |
||||
const version = 26 |
||||
|
||||
/* |
||||
|
||||
This migration moves the identities stored in the KeyringController |
||||
into the PreferencesController |
||||
|
||||
*/ |
||||
|
||||
const clone = require('clone') |
||||
|
||||
module.exports = { |
||||
version, |
||||
migrate (originalVersionedData) { |
||||
const versionedData = clone(originalVersionedData) |
||||
versionedData.meta.version = version |
||||
try { |
||||
const state = versionedData.data |
||||
versionedData.data = transformState(state) |
||||
} catch (err) { |
||||
console.warn(`MetaMask Migration #${version}` + err.stack) |
||||
return Promise.reject(err) |
||||
} |
||||
return Promise.resolve(versionedData) |
||||
}, |
||||
} |
||||
|
||||
function transformState (state) { |
||||
if (!state.KeyringController || !state.PreferencesController) { |
||||
return |
||||
} |
||||
|
||||
if (!state.KeyringController.walletNicknames) { |
||||
return state |
||||
} |
||||
|
||||
state.PreferencesController.identities = Object.keys(state.KeyringController.walletNicknames) |
||||
.reduce((identities, address) => { |
||||
identities[address] = { |
||||
name: state.KeyringController.walletNicknames[address], |
||||
address, |
||||
} |
||||
return identities |
||||
}, {}) |
||||
delete state.KeyringController.walletNicknames |
||||
return state |
||||
} |
File diff suppressed because it is too large
Load Diff
@ -1,314 +0,0 @@ |
||||
const fs = require('fs') |
||||
const mkdirp = require('mkdirp') |
||||
const path = require('path') |
||||
const assert = require('assert') |
||||
const pify = require('pify') |
||||
const webdriver = require('selenium-webdriver') |
||||
const until = require('selenium-webdriver/lib/until') |
||||
const By = webdriver.By |
||||
const { delay, buildChromeWebDriver } = require('../func') |
||||
|
||||
describe('Metamask popup page', function () { |
||||
let driver, accountAddress, tokenAddress, extensionId |
||||
|
||||
this.timeout(0) |
||||
|
||||
before(async function () { |
||||
const extPath = path.resolve('dist/chrome') |
||||
driver = buildChromeWebDriver(extPath) |
||||
await driver.get('chrome://extensions') |
||||
await delay(500) |
||||
}) |
||||
|
||||
afterEach(async function () { |
||||
if (this.currentTest.state === 'failed') { |
||||
await verboseReportOnFailure(this.currentTest) |
||||
} |
||||
}) |
||||
|
||||
after(async function () { |
||||
await driver.quit() |
||||
}) |
||||
|
||||
describe('Setup', function () { |
||||
|
||||
it('switches to Chrome extensions list', async function () { |
||||
const tabs = await driver.getAllWindowHandles() |
||||
await driver.switchTo().window(tabs[0]) |
||||
await delay(300) |
||||
}) |
||||
|
||||
it(`selects MetaMask's extension id and opens it in the current tab`, async function () { |
||||
extensionId = await getExtensionId() |
||||
await driver.get(`chrome-extension://${extensionId}/popup.html`) |
||||
await delay(500) |
||||
}) |
||||
|
||||
it('sets provider type to localhost', async function () { |
||||
await driver.wait(until.elementLocated(By.css('#app-content')), 300) |
||||
await setProviderType('localhost') |
||||
}) |
||||
}) |
||||
|
||||
describe('Account Creation', () => { |
||||
|
||||
it('matches MetaMask title', async () => { |
||||
const title = await driver.getTitle() |
||||
assert.equal(title, 'MetaMask', 'title matches MetaMask') |
||||
}) |
||||
|
||||
it('shows privacy notice', async () => { |
||||
await driver.wait(async () => { |
||||
const privacyHeader = await driver.findElement(By.css('#app-content > div > div.app-primary.from-right > div > div.flex-column.flex-center.flex-grow > h3')).getText() |
||||
assert.equal(privacyHeader, 'PRIVACY NOTICE', 'shows privacy notice')
|
||||
return privacyHeader === 'PRIVACY NOTICE' |
||||
}, 300) |
||||
await driver.findElement(By.css('button')).click() |
||||
}) |
||||
|
||||
it('show terms of use', async () => { |
||||
await driver.wait(async () => { |
||||
const terms = await driver.findElement(By.css('#app-content > div > div.app-primary.from-right > div > div.flex-column.flex-center.flex-grow > h3')).getText() |
||||
assert.equal(terms, 'TERMS OF USE', 'shows terms of use') |
||||
return terms === 'TERMS OF USE' |
||||
}) |
||||
}) |
||||
|
||||
it('checks if the TOU button is disabled', async () => { |
||||
const button = await driver.findElement(By.css('button')).isEnabled() |
||||
assert.equal(button, false, 'disabled continue button') |
||||
const element = await driver.findElement(By.linkText('Attributions')) |
||||
await driver.executeScript('arguments[0].scrollIntoView(true)', element) |
||||
await delay(300) |
||||
}) |
||||
|
||||
it('allows the button to be clicked when scrolled to the bottom of TOU', async () => { |
||||
const button = await driver.findElement(By.css('#app-content > div > div.app-primary.from-right > div > div.flex-column.flex-center.flex-grow > button')) |
||||
const buttonEnabled = await button.isEnabled() |
||||
assert.equal(buttonEnabled, true, 'enabled continue button') |
||||
await button.click() |
||||
}) |
||||
|
||||
it('accepts password with length of eight', async () => { |
||||
const passwordBox = await driver.findElement(By.id('password-box')) |
||||
const passwordBoxConfirm = await driver.findElement(By.id('password-box-confirm')) |
||||
const button = await driver.findElements(By.css('button')) |
||||
|
||||
await passwordBox.sendKeys('123456789') |
||||
await passwordBoxConfirm.sendKeys('123456789') |
||||
await button[0].click() |
||||
await delay(500) |
||||
}) |
||||
|
||||
it('shows value was created and seed phrase', async () => { |
||||
await delay(300) |
||||
const seedPhrase = await driver.findElement(By.css('.twelve-word-phrase')).getText() |
||||
assert.equal(seedPhrase.split(' ').length, 12) |
||||
const continueAfterSeedPhrase = await driver.findElement(By.css('#app-content > div > div.app-primary.from-right > div > button:nth-child(4)')) |
||||
assert.equal(await continueAfterSeedPhrase.getText(), `I'VE COPIED IT SOMEWHERE SAFE`) |
||||
await continueAfterSeedPhrase.click() |
||||
await delay(300) |
||||
}) |
||||
|
||||
it('shows account address', async function () { |
||||
accountAddress = await driver.findElement(By.css('#app-content > div > div.app-primary.from-left > div > div > div:nth-child(1) > flex-column > div.flex-row > div')).getText() |
||||
}) |
||||
|
||||
it('logs out of the vault', async () => { |
||||
await driver.findElement(By.css('.sandwich-expando')).click() |
||||
await delay(500) |
||||
const logoutButton = await driver.findElement(By.css('#app-content > div > div:nth-child(3) > span > div > li:nth-child(3)')) |
||||
assert.equal(await logoutButton.getText(), 'Log Out') |
||||
await logoutButton.click() |
||||
}) |
||||
|
||||
it('accepts account password after lock', async () => { |
||||
await delay(500) |
||||
await driver.findElement(By.id('password-box')).sendKeys('123456789') |
||||
await driver.findElement(By.css('button')).click() |
||||
await delay(500) |
||||
}) |
||||
|
||||
it('shows QR code option', async () => { |
||||
await delay(300) |
||||
await driver.findElement(By.css('.fa-ellipsis-h')).click() |
||||
await driver.findElement(By.css('#app-content > div > div.app-primary.from-right > div > div > div:nth-child(1) > flex-column > div.name-label > div > span > i > div > div > li:nth-child(3)')).click() |
||||
await delay(300) |
||||
}) |
||||
|
||||
it('checks QR code address is the same as account details address', async () => { |
||||
const QRaccountAddress = await driver.findElement(By.css('.ellip-address')).getText() |
||||
assert.equal(accountAddress.toLowerCase(), QRaccountAddress) |
||||
await driver.findElement(By.css('.fa-arrow-left')).click() |
||||
await delay(500) |
||||
}) |
||||
}) |
||||
|
||||
describe('Import Ganache seed phrase', function () { |
||||
it('logs out', async function () { |
||||
await driver.findElement(By.css('.sandwich-expando')).click() |
||||
await delay(200) |
||||
const logOut = await driver.findElement(By.css('#app-content > div > div:nth-child(3) > span > div > li:nth-child(3)')) |
||||
assert.equal(await logOut.getText(), 'Log Out') |
||||
await logOut.click() |
||||
await delay(300) |
||||
}) |
||||
|
||||
it('restores from seed phrase', async function () { |
||||
const restoreSeedLink = await driver.findElement(By.css('#app-content > div > div.app-primary.from-left > div > div.flex-row.flex-center.flex-grow > p')) |
||||
assert.equal(await restoreSeedLink.getText(), 'Restore from seed phrase') |
||||
await restoreSeedLink.click() |
||||
await delay(100) |
||||
}) |
||||
|
||||
it('adds seed phrase', async function () { |
||||
const testSeedPhrase = 'phrase upgrade clock rough situate wedding elder clever doctor stamp excess tent' |
||||
const seedTextArea = await driver.findElement(By.css('#app-content > div > div.app-primary.from-left > div > textarea')) |
||||
await seedTextArea.sendKeys(testSeedPhrase) |
||||
|
||||
await driver.findElement(By.id('password-box')).sendKeys('123456789') |
||||
await driver.findElement(By.id('password-box-confirm')).sendKeys('123456789') |
||||
await driver.findElement(By.css('#app-content > div > div.app-primary.from-left > div > div > button:nth-child(2)')).click() |
||||
await delay(500) |
||||
}) |
||||
|
||||
it('balance renders', async function () { |
||||
await delay(200) |
||||
const balance = await driver.findElement(By.css('#app-content > div > div.app-primary.from-right > div > div > div.flex-row > div.ether-balance.ether-balance-amount > div > div > div:nth-child(1) > div:nth-child(1)')) |
||||
assert.equal(await balance.getText(), '100.000') |
||||
await delay(200) |
||||
}) |
||||
|
||||
it('sends transaction', async function () { |
||||
const sendButton = await driver.findElement(By.css('#app-content > div > div.app-primary.from-right > div > div > div.flex-row > button:nth-child(4)')) |
||||
assert.equal(await sendButton.getText(), 'SEND') |
||||
await sendButton.click() |
||||
await delay(200) |
||||
}) |
||||
|
||||
it('adds recipient address and amount', async function () { |
||||
const sendTranscationScreen = await driver.findElement(By.css('#app-content > div > div.app-primary.from-right > div > h3:nth-child(2)')).getText() |
||||
assert.equal(sendTranscationScreen, 'SEND TRANSACTION') |
||||
const inputAddress = await driver.findElement(By.css('#app-content > div > div.app-primary.from-right > div > section:nth-child(3) > div > input')) |
||||
const inputAmmount = await driver.findElement(By.css('#app-content > div > div.app-primary.from-right > div > section:nth-child(4) > input')) |
||||
await inputAddress.sendKeys('0x2f318C334780961FB129D2a6c30D0763d9a5C970') |
||||
await inputAmmount.sendKeys('10') |
||||
await driver.findElement(By.css('#app-content > div > div.app-primary.from-right > div > section:nth-child(4) > button')).click() |
||||
await delay(300) |
||||
}) |
||||
|
||||
it('confirms transaction', async function () { |
||||
await delay(300) |
||||
await driver.findElement(By.css('#pending-tx-form > div.flex-row.flex-space-around.conf-buttons > input')).click() |
||||
await delay(500) |
||||
}) |
||||
|
||||
it('finds the transaction in the transactions list', async function () { |
||||
const tranasactionAmount = await driver.findElement(By.css('#app-content > div > div.app-primary.from-left > div > section > section > div > div > div > div.ether-balance.ether-balance-amount > div > div > div > div:nth-child(1)')) |
||||
assert.equal(await tranasactionAmount.getText(), '10.0') |
||||
}) |
||||
}) |
||||
|
||||
describe('Token Factory', function () { |
||||
|
||||
it('navigates to token factory', async function () { |
||||
await driver.get('http://tokenfactory.surge.sh/') |
||||
}) |
||||
|
||||
it('navigates to create token contract link', async function () { |
||||
const createToken = await driver.findElement(By.css('#bs-example-navbar-collapse-1 > ul > li:nth-child(3) > a')) |
||||
await createToken.click() |
||||
}) |
||||
|
||||
it('adds input for token', async function () { |
||||
const totalSupply = await driver.findElement(By.css('#main > div > div > div > div:nth-child(2) > div > div:nth-child(5) > input')) |
||||
const tokenName = await driver.findElement(By.css('#main > div > div > div > div:nth-child(2) > div > div:nth-child(6) > input')) |
||||
const tokenDecimal = await driver.findElement(By.css('#main > div > div > div > div:nth-child(2) > div > div:nth-child(7) > input')) |
||||
const tokenSymbol = await driver.findElement(By.css('#main > div > div > div > div:nth-child(2) > div > div:nth-child(8) > input')) |
||||
const createToken = await driver.findElement(By.css('#main > div > div > div > div:nth-child(2) > div > button')) |
||||
|
||||
await totalSupply.sendKeys('100') |
||||
await tokenName.sendKeys('Test') |
||||
await tokenDecimal.sendKeys('0') |
||||
await tokenSymbol.sendKeys('TST') |
||||
await createToken.click() |
||||
await delay(1000) |
||||
}) |
||||
|
||||
it('confirms transaction in MetaMask popup', async function () { |
||||
const windowHandles = await driver.getAllWindowHandles() |
||||
await driver.switchTo().window(windowHandles[windowHandles.length - 1]) |
||||
const metamaskSubmit = await driver.findElement(By.css('#pending-tx-form > div.flex-row.flex-space-around.conf-buttons > input')) |
||||
await metamaskSubmit.click() |
||||
await delay(1000) |
||||
}) |
||||
|
||||
it('switches back to Token Factory to grab the token contract address', async function () { |
||||
const windowHandles = await driver.getAllWindowHandles() |
||||
await driver.switchTo().window(windowHandles[0]) |
||||
const tokenContactAddress = await driver.findElement(By.css('#main > div > div > div > div:nth-child(2) > span:nth-child(3)')) |
||||
tokenAddress = await tokenContactAddress.getText() |
||||
await delay(500) |
||||
}) |
||||
|
||||
it('navigates back to MetaMask popup in the tab', async function () { |
||||
await driver.get(`chrome-extension://${extensionId}/popup.html`) |
||||
await delay(700) |
||||
}) |
||||
}) |
||||
|
||||
describe('Add Token', function () { |
||||
it('switches to the add token screen', async function () { |
||||
const tokensTab = await driver.findElement(By.css('#app-content > div > div.app-primary.from-right > div > section > div > div.inactiveForm.pointer')) |
||||
assert.equal(await tokensTab.getText(), 'TOKENS') |
||||
await tokensTab.click() |
||||
await delay(300) |
||||
}) |
||||
|
||||
it('navigates to the add token screen', async function () { |
||||
const addTokenButton = await driver.findElement(By.css('#app-content > div > div.app-primary.from-right > div > section > div.full-flex-height > div > button')) |
||||
assert.equal(await addTokenButton.getText(), 'ADD TOKEN') |
||||
await addTokenButton.click() |
||||
}) |
||||
|
||||
it('checks add token screen rendered', async function () { |
||||
const addTokenScreen = await driver.findElement(By.css('#app-content > div > div.app-primary.from-right > div > div.section-title.flex-row.flex-center > h2')) |
||||
assert.equal(await addTokenScreen.getText(), 'ADD TOKEN') |
||||
}) |
||||
|
||||
it('adds token parameters', async function () { |
||||
const tokenContractAddress = await driver.findElement(By.css('#token-address')) |
||||
await tokenContractAddress.sendKeys(tokenAddress) |
||||
await delay(300) |
||||
await driver.findElement(By.css('#app-content > div > div.app-primary.from-right > div > div.flex-column.flex-justify-center.flex-grow.select-none > div > button')).click() |
||||
await delay(100) |
||||
}) |
||||
|
||||
it('checks the token balance', async function () { |
||||
const tokenBalance = await driver.findElement(By.css('#app-content > div > div.app-primary.from-left > div > section > div.full-flex-height > ol > li:nth-child(2) > h3')) |
||||
assert.equal(await tokenBalance.getText(), '100 TST') |
||||
}) |
||||
}) |
||||
|
||||
async function getExtensionId () { |
||||
const extension = await driver.executeScript('return document.querySelector("extensions-manager").shadowRoot.querySelector("extensions-view-manager extensions-item-list").shadowRoot.querySelector("extensions-item:nth-child(2)").getAttribute("id")') |
||||
return extension |
||||
} |
||||
|
||||
async function setProviderType (type) { |
||||
await driver.executeScript('window.metamask.setProviderType(arguments[0])', type) |
||||
} |
||||
|
||||
async function verboseReportOnFailure (test) { |
||||
const artifactDir = `./test-artifacts/chrome/${test.title}` |
||||
const filepathBase = `${artifactDir}/test-failure` |
||||
await pify(mkdirp)(artifactDir) |
||||
// capture screenshot
|
||||
const screenshot = await driver.takeScreenshot() |
||||
await pify(fs.writeFile)(`${filepathBase}-screenshot.png`, screenshot, { encoding: 'base64' }) |
||||
// capture dom source
|
||||
const htmlSource = await driver.getPageSource() |
||||
await pify(fs.writeFile)(`${filepathBase}-dom.html`, htmlSource) |
||||
} |
||||
|
||||
}) |
File diff suppressed because it is too large
Load Diff
@ -1,35 +0,0 @@ |
||||
// var jsdom = require('mocha-jsdom')
|
||||
var assert = require('assert') |
||||
var freeze = require('deep-freeze-strict') |
||||
var path = require('path') |
||||
|
||||
var actions = require(path.join(__dirname, '..', '..', '..', 'ui', 'app', 'actions.js')) |
||||
var reducers = require(path.join(__dirname, '..', '..', '..', 'ui', 'app', 'reducers.js')) |
||||
|
||||
describe('SAVE_ACCOUNT_LABEL', function () { |
||||
it('updates the state.metamask.identities[:i].name property of the state to the action.value.label', function () { |
||||
var initialState = { |
||||
metamask: { |
||||
identities: { |
||||
foo: { |
||||
name: 'bar', |
||||
}, |
||||
}, |
||||
}, |
||||
} |
||||
freeze(initialState) |
||||
|
||||
const action = { |
||||
type: actions.SAVE_ACCOUNT_LABEL, |
||||
value: { |
||||
account: 'foo', |
||||
label: 'baz', |
||||
}, |
||||
} |
||||
freeze(action) |
||||
|
||||
var resultingState = reducers(initialState, action) |
||||
assert.equal(resultingState.metamask.identities.foo.name, action.value.label) |
||||
}) |
||||
}) |
||||
|
@ -0,0 +1,34 @@ |
||||
const assert = require('assert') |
||||
const freeze = require('deep-freeze-strict') |
||||
const path = require('path') |
||||
|
||||
const actions = require(path.join(__dirname, '..', '..', '..', 'ui', 'app', 'actions.js')) |
||||
const reducers = require(path.join(__dirname, '..', '..', '..', 'ui', 'app', 'reducers.js')) |
||||
|
||||
describe('SET_ACCOUNT_LABEL', function () { |
||||
it('updates the state.metamask.identities[:i].name property of the state to the action.value.label', function () { |
||||
const initialState = { |
||||
metamask: { |
||||
identities: { |
||||
foo: { |
||||
name: 'bar', |
||||
}, |
||||
}, |
||||
}, |
||||
} |
||||
freeze(initialState) |
||||
|
||||
const action = { |
||||
type: actions.SET_ACCOUNT_LABEL, |
||||
value: { |
||||
account: 'foo', |
||||
label: 'baz', |
||||
}, |
||||
} |
||||
freeze(action) |
||||
|
||||
const resultingState = reducers(initialState, action) |
||||
assert.equal(resultingState.metamask.identities.foo.name, action.value.label) |
||||
}) |
||||
}) |
||||
|
@ -1,5 +1,5 @@ |
||||
const assert = require('assert') |
||||
const ComposableObservableStore = require('../../app/scripts/lib/ComposableObservableStore') |
||||
const ComposableObservableStore = require('../../../app/scripts/lib/ComposableObservableStore') |
||||
const ObservableStore = require('obs-store') |
||||
|
||||
describe('ComposableObservableStore', () => { |
@ -0,0 +1,31 @@ |
||||
const assert = require('assert') |
||||
const path = require('path') |
||||
const accountImporter = require('../../../app/scripts/account-import-strategies/index') |
||||
const ethUtil = require('ethereumjs-util') |
||||
|
||||
describe('Account Import Strategies', function () { |
||||
const privkey = '0x4cfd3e90fc78b0f86bf7524722150bb8da9c60cd532564d7ff43f5716514f553' |
||||
const json = '{"version":3,"id":"dbb54385-0a99-437f-83c0-647de9f244c3","address":"a7f92ce3fba24196cf6f4bd2e1eb3db282ba998c","Crypto":{"ciphertext":"bde13d9ade5c82df80281ca363320ce254a8a3a06535bbf6ffdeaf0726b1312c","cipherparams":{"iv":"fbf93718a57f26051b292f072f2e5b41"},"cipher":"aes-128-ctr","kdf":"scrypt","kdfparams":{"dklen":32,"salt":"7ffe00488319dec48e4c49a120ca49c6afbde9272854c64d9541c83fc6acdffe","n":8192,"r":8,"p":1},"mac":"2adfd9c4bc1cdac4c85bddfb31d9e21a684e0e050247a70c5698facf6b7d4681"}}' |
||||
|
||||
it('imports a private key and strips 0x prefix', async function () { |
||||
const importPrivKey = await accountImporter.importAccount('Private Key', [ privkey ]) |
||||
assert.equal(importPrivKey, ethUtil.stripHexPrefix(privkey)) |
||||
}) |
||||
|
||||
it('fails when password is incorrect for keystore', async function () { |
||||
const wrongPassword = 'password2' |
||||
|
||||
try { |
||||
await accountImporter.importAccount('JSON File', [ json, wrongPassword]) |
||||
} catch (error) { |
||||
assert.equal(error.message, 'Key derivation failed - possibly wrong passphrase') |
||||
} |
||||
}) |
||||
|
||||
it('imports json string and password to return a private key', async function () { |
||||
const fileContentsPassword = 'password1' |
||||
const importJson = await accountImporter.importAccount('JSON File', [ json, fileContentsPassword]) |
||||
assert.equal(importJson, '0x5733876abe94146069ce8bcbabbde2677f2e35fa33e875e92041ed2ac87e5bc7') |
||||
}) |
||||
|
||||
}) |
@ -0,0 +1,48 @@ |
||||
const assert = require('assert') |
||||
const getBuyEthUrl = require('../../../app/scripts/lib/buy-eth-url') |
||||
|
||||
describe('', function () { |
||||
const mainnet = { |
||||
network: '1', |
||||
amount: 5, |
||||
address: '0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc', |
||||
} |
||||
const ropsten = { |
||||
network: '3', |
||||
} |
||||
const rinkeby = { |
||||
network: '4', |
||||
} |
||||
const kovan = { |
||||
network: '42', |
||||
} |
||||
|
||||
it('returns coinbase url with amount and address for network 1', function () { |
||||
const coinbaseUrl = getBuyEthUrl(mainnet) |
||||
const coinbase = coinbaseUrl.match(/(https:\/\/buy.coinbase.com)/) |
||||
const amount = coinbaseUrl.match(/(amount)\D\d/) |
||||
const address = coinbaseUrl.match(/(address)(.*)(?=&)/) |
||||
|
||||
assert.equal(coinbase[0], 'https://buy.coinbase.com') |
||||
assert.equal(amount[0], 'amount=5') |
||||
assert.equal(address[0], 'address=0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc') |
||||
|
||||
}) |
||||
|
||||
it('returns metamask ropsten faucet for network 3', function () { |
||||
const ropstenUrl = getBuyEthUrl(ropsten) |
||||
assert.equal(ropstenUrl, 'https://faucet.metamask.io/') |
||||
}) |
||||
|
||||
it('returns rinkeby dapp for network 4', function () { |
||||
const rinkebyUrl = getBuyEthUrl(rinkeby) |
||||
assert.equal(rinkebyUrl, 'https://www.rinkeby.io/') |
||||
}) |
||||
|
||||
it('returns kovan github test faucet for network 42', function () { |
||||
const kovanUrl = getBuyEthUrl(kovan) |
||||
assert.equal(kovanUrl, 'https://github.com/kovan-testnet/faucet') |
||||
}) |
||||
|
||||
}) |
||||
|
@ -1,5 +1,5 @@ |
||||
const assert = require('assert') |
||||
const BlacklistController = require('../../app/scripts/controllers/blacklist') |
||||
const BlacklistController = require('../../../../app/scripts/controllers/blacklist') |
||||
|
||||
describe('blacklist controller', function () { |
||||
let blacklistController |
@ -0,0 +1,550 @@ |
||||
const assert = require('assert') |
||||
const sinon = require('sinon') |
||||
const clone = require('clone') |
||||
const nock = require('nock') |
||||
const createThoughStream = require('through2').obj |
||||
const MetaMaskController = require('../../../../app/scripts/metamask-controller') |
||||
const blacklistJSON = require('eth-phishing-detect/src/config') |
||||
const firstTimeState = require('../../../../app/scripts/first-time-state') |
||||
|
||||
const currentNetworkId = 42 |
||||
const DEFAULT_LABEL = 'Account 1' |
||||
const TEST_SEED = 'debris dizzy just program just float decrease vacant alarm reduce speak stadium' |
||||
const TEST_ADDRESS = '0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc' |
||||
const TEST_SEED_ALT = 'setup olympic issue mobile velvet surge alcohol burger horse view reopen gentle' |
||||
const TEST_ADDRESS_ALT = '0xc42edfcc21ed14dda456aa0756c153f7985d8813' |
||||
|
||||
describe('MetaMaskController', function () { |
||||
let metamaskController |
||||
const sandbox = sinon.createSandbox() |
||||
const noop = () => {} |
||||
|
||||
beforeEach(function () { |
||||
|
||||
nock('https://api.infura.io') |
||||
.persist() |
||||
.get('/v2/blacklist') |
||||
.reply(200, blacklistJSON) |
||||
|
||||
nock('https://api.infura.io') |
||||
.get('/v1/ticker/ethusd') |
||||
.reply(200, '{"base": "ETH", "quote": "USD", "bid": 288.45, "ask": 288.46, "volume": 112888.17569277, "exchange": "bitfinex", "total_volume": 272175.00106721005, "num_exchanges": 8, "timestamp": 1506444677}') |
||||
|
||||
nock('https://api.infura.io') |
||||
.get('/v1/ticker/ethjpy') |
||||
.reply(200, '{"base": "ETH", "quote": "JPY", "bid": 32300.0, "ask": 32400.0, "volume": 247.4616071, "exchange": "kraken", "total_volume": 247.4616071, "num_exchanges": 1, "timestamp": 1506444676}') |
||||
|
||||
nock('https://api.infura.io') |
||||
.persist() |
||||
.get(/.*/) |
||||
.reply(200) |
||||
|
||||
metamaskController = new MetaMaskController({ |
||||
showUnapprovedTx: noop, |
||||
showUnconfirmedMessage: noop, |
||||
encryptor: { |
||||
encrypt: function (password, object) { |
||||
this.object = object |
||||
return Promise.resolve() |
||||
}, |
||||
decrypt: function () { |
||||
return Promise.resolve(this.object) |
||||
}, |
||||
}, |
||||
initState: clone(firstTimeState), |
||||
}) |
||||
sandbox.spy(metamaskController.keyringController, 'createNewVaultAndKeychain') |
||||
sandbox.spy(metamaskController.keyringController, 'createNewVaultAndRestore') |
||||
}) |
||||
|
||||
afterEach(function () { |
||||
nock.cleanAll() |
||||
sandbox.restore() |
||||
}) |
||||
|
||||
describe('#getGasPrice', function () { |
||||
|
||||
it('gives the 50th percentile lowest accepted gas price from recentBlocksController', async function () { |
||||
const realRecentBlocksController = metamaskController.recentBlocksController |
||||
metamaskController.recentBlocksController = { |
||||
store: { |
||||
getState: () => { |
||||
return { |
||||
recentBlocks: [ |
||||
{ gasPrices: [ '0x3b9aca00', '0x174876e800'] }, |
||||
{ gasPrices: [ '0x3b9aca00', '0x174876e800'] }, |
||||
{ gasPrices: [ '0x174876e800', '0x174876e800' ]}, |
||||
{ gasPrices: [ '0x174876e800', '0x174876e800' ]}, |
||||
], |
||||
} |
||||
}, |
||||
}, |
||||
} |
||||
|
||||
const gasPrice = metamaskController.getGasPrice() |
||||
assert.equal(gasPrice, '0x3b9aca00', 'accurately estimates 50th percentile accepted gas price') |
||||
|
||||
metamaskController.recentBlocksController = realRecentBlocksController |
||||
}) |
||||
}) |
||||
|
||||
describe('#createNewVaultAndKeychain', function () { |
||||
it('can only create new vault on keyringController once', async function () { |
||||
const selectStub = sandbox.stub(metamaskController, 'selectFirstIdentity') |
||||
|
||||
const password = 'a-fake-password' |
||||
|
||||
await metamaskController.createNewVaultAndKeychain(password) |
||||
await metamaskController.createNewVaultAndKeychain(password) |
||||
|
||||
assert(metamaskController.keyringController.createNewVaultAndKeychain.calledOnce) |
||||
|
||||
selectStub.reset() |
||||
}) |
||||
}) |
||||
|
||||
describe('#createNewVaultAndRestore', function () { |
||||
it('should be able to call newVaultAndRestore despite a mistake.', async function () { |
||||
const password = 'what-what-what' |
||||
await metamaskController.createNewVaultAndRestore(password, TEST_SEED.slice(0, -1)).catch((e) => null) |
||||
await metamaskController.createNewVaultAndRestore(password, TEST_SEED) |
||||
|
||||
assert(metamaskController.keyringController.createNewVaultAndRestore.calledTwice) |
||||
}) |
||||
|
||||
it('should clear previous identities after vault restoration', async () => { |
||||
await metamaskController.createNewVaultAndRestore('foobar1337', TEST_SEED) |
||||
assert.deepEqual(metamaskController.getState().identities, { |
||||
[TEST_ADDRESS]: { address: TEST_ADDRESS, name: DEFAULT_LABEL }, |
||||
}) |
||||
|
||||
await metamaskController.preferencesController.setAccountLabel(TEST_ADDRESS, 'Account Foo') |
||||
assert.deepEqual(metamaskController.getState().identities, { |
||||
[TEST_ADDRESS]: { address: TEST_ADDRESS, name: 'Account Foo' }, |
||||
}) |
||||
|
||||
await metamaskController.createNewVaultAndRestore('foobar1337', TEST_SEED_ALT) |
||||
assert.deepEqual(metamaskController.getState().identities, { |
||||
[TEST_ADDRESS_ALT]: { address: TEST_ADDRESS_ALT, name: DEFAULT_LABEL }, |
||||
}) |
||||
}) |
||||
}) |
||||
|
||||
describe('#getApi', function () { |
||||
let getApi, state |
||||
|
||||
beforeEach(function () { |
||||
getApi = metamaskController.getApi() |
||||
}) |
||||
|
||||
it('getState', function (done) { |
||||
getApi.getState((err, res) => { |
||||
if (err) { |
||||
done(err) |
||||
} else { |
||||
state = res |
||||
} |
||||
}) |
||||
assert.deepEqual(state, metamaskController.getState()) |
||||
done() |
||||
}) |
||||
|
||||
}) |
||||
|
||||
describe('preferencesController', function () { |
||||
|
||||
it('defaults useBlockie to false', function () { |
||||
assert.equal(metamaskController.preferencesController.store.getState().useBlockie, false) |
||||
}) |
||||
|
||||
it('setUseBlockie to true', function () { |
||||
metamaskController.setUseBlockie(true, noop) |
||||
assert.equal(metamaskController.preferencesController.store.getState().useBlockie, true) |
||||
}) |
||||
|
||||
}) |
||||
|
||||
describe('#selectFirstIdentity', function () { |
||||
let identities, address |
||||
|
||||
beforeEach(function () { |
||||
address = '0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc' |
||||
identities = { |
||||
'0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc': { |
||||
'address': address, |
||||
'name': 'Account 1', |
||||
}, |
||||
'0xc42edfcc21ed14dda456aa0756c153f7985d8813': { |
||||
'address': '0xc42edfcc21ed14dda456aa0756c153f7985d8813', |
||||
'name': 'Account 2', |
||||
}, |
||||
} |
||||
metamaskController.preferencesController.store.updateState({ identities }) |
||||
metamaskController.selectFirstIdentity() |
||||
}) |
||||
|
||||
it('changes preferences controller select address', function () { |
||||
const preferenceControllerState = metamaskController.preferencesController.store.getState() |
||||
assert.equal(preferenceControllerState.selectedAddress, address) |
||||
}) |
||||
|
||||
it('changes metamask controller selected address', function () { |
||||
const metamaskState = metamaskController.getState() |
||||
assert.equal(metamaskState.selectedAddress, address) |
||||
}) |
||||
}) |
||||
|
||||
describe('#setCustomRpc', function () { |
||||
const customRPC = 'https://custom.rpc/' |
||||
let rpcTarget |
||||
|
||||
beforeEach(function () { |
||||
|
||||
nock('https://custom.rpc') |
||||
.post('/') |
||||
.reply(200) |
||||
|
||||
rpcTarget = metamaskController.setCustomRpc(customRPC) |
||||
}) |
||||
|
||||
afterEach(function () { |
||||
nock.cleanAll() |
||||
}) |
||||
|
||||
it('returns custom RPC that when called', async function () { |
||||
assert.equal(await rpcTarget, customRPC) |
||||
}) |
||||
|
||||
it('changes the network controller rpc', function () { |
||||
const networkControllerState = metamaskController.networkController.store.getState() |
||||
assert.equal(networkControllerState.provider.rpcTarget, customRPC) |
||||
}) |
||||
}) |
||||
|
||||
describe('#setCurrentCurrency', function () { |
||||
let defaultMetaMaskCurrency |
||||
|
||||
beforeEach(function () { |
||||
defaultMetaMaskCurrency = metamaskController.currencyController.getCurrentCurrency() |
||||
}) |
||||
|
||||
it('defaults to usd', function () { |
||||
assert.equal(defaultMetaMaskCurrency, 'usd') |
||||
}) |
||||
|
||||
it('sets currency to JPY', function () { |
||||
metamaskController.setCurrentCurrency('JPY', noop) |
||||
assert.equal(metamaskController.currencyController.getCurrentCurrency(), 'JPY') |
||||
}) |
||||
}) |
||||
|
||||
describe('#createShapeshifttx', function () { |
||||
let depositAddress, depositType, shapeShiftTxList |
||||
|
||||
beforeEach(function () { |
||||
nock('https://shapeshift.io') |
||||
.get('/txStat/3EevLFfB4H4XMWQwYCgjLie1qCAGpd2WBc') |
||||
.reply(200, '{"status": "no_deposits", "address": "3EevLFfB4H4XMWQwYCgjLie1qCAGpd2WBc"}') |
||||
|
||||
depositAddress = '3EevLFfB4H4XMWQwYCgjLie1qCAGpd2WBc' |
||||
depositType = 'ETH' |
||||
shapeShiftTxList = metamaskController.shapeshiftController.store.getState().shapeShiftTxList |
||||
}) |
||||
|
||||
it('creates a shapeshift tx', async function () { |
||||
metamaskController.createShapeShiftTx(depositAddress, depositType) |
||||
assert.equal(shapeShiftTxList[0].depositAddress, depositAddress) |
||||
}) |
||||
|
||||
}) |
||||
|
||||
describe('#addNewAccount', function () { |
||||
let addNewAccount |
||||
|
||||
beforeEach(function () { |
||||
addNewAccount = metamaskController.addNewAccount() |
||||
}) |
||||
|
||||
it('errors when an primary keyring is does not exist', async function () { |
||||
try { |
||||
await addNewAccount |
||||
assert.equal(1 === 0) |
||||
} catch (e) { |
||||
assert.equal(e.message, 'MetamaskController - No HD Key Tree found') |
||||
} |
||||
}) |
||||
}) |
||||
|
||||
describe('#verifyseedPhrase', function () { |
||||
let seedPhrase, getConfigSeed |
||||
|
||||
it('errors when no keying is provided', async function () { |
||||
try { |
||||
await metamaskController.verifySeedPhrase() |
||||
} catch (error) { |
||||
assert.equal(error.message, 'MetamaskController - No HD Key Tree found') |
||||
} |
||||
}) |
||||
|
||||
beforeEach(async function () { |
||||
await metamaskController.createNewVaultAndKeychain('password') |
||||
seedPhrase = await metamaskController.verifySeedPhrase() |
||||
}) |
||||
|
||||
it('#placeSeedWords should match the initially created vault seed', function () { |
||||
|
||||
metamaskController.placeSeedWords((err, result) => { |
||||
if (err) { |
||||
console.log(err) |
||||
} else { |
||||
getConfigSeed = metamaskController.configManager.getSeedWords() |
||||
assert.equal(result, seedPhrase) |
||||
assert.equal(result, getConfigSeed) |
||||
} |
||||
}) |
||||
assert.equal(getConfigSeed, undefined) |
||||
}) |
||||
|
||||
it('#addNewAccount', async function () { |
||||
await metamaskController.addNewAccount() |
||||
const getAccounts = await metamaskController.keyringController.getAccounts() |
||||
assert.equal(getAccounts.length, 2) |
||||
}) |
||||
}) |
||||
|
||||
describe('#resetAccount', function () { |
||||
|
||||
beforeEach(function () { |
||||
const selectedAddressStub = sinon.stub(metamaskController.preferencesController, 'getSelectedAddress') |
||||
const getNetworkstub = sinon.stub(metamaskController.txController.txStateManager, 'getNetwork') |
||||
|
||||
selectedAddressStub.returns('0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc') |
||||
getNetworkstub.returns(42) |
||||
|
||||
metamaskController.txController.txStateManager._saveTxList([ |
||||
{ id: 1, status: 'unapproved', metamaskNetworkId: currentNetworkId, txParams: {from: '0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc'} }, |
||||
{ id: 2, status: 'rejected', metamaskNetworkId: 32, txParams: {} }, |
||||
{ id: 3, status: 'submitted', metamaskNetworkId: currentNetworkId, txParams: {from: '0xB09d8505E1F4EF1CeA089D47094f5DD3464083d4'} }, |
||||
]) |
||||
}) |
||||
|
||||
it('wipes transactions from only the correct network id and with the selected address', async function () { |
||||
await metamaskController.resetAccount() |
||||
assert.equal(metamaskController.txController.txStateManager.getTx(1), undefined) |
||||
}) |
||||
}) |
||||
|
||||
describe('#clearSeedWordCache', function () { |
||||
|
||||
it('should have set seed words', function () { |
||||
metamaskController.configManager.setSeedWords('test words') |
||||
const getConfigSeed = metamaskController.configManager.getSeedWords() |
||||
assert.equal(getConfigSeed, 'test words') |
||||
}) |
||||
|
||||
it('should clear config seed phrase', function () { |
||||
metamaskController.configManager.setSeedWords('test words') |
||||
metamaskController.clearSeedWordCache((err, result) => { |
||||
if (err) console.log(err) |
||||
}) |
||||
const getConfigSeed = metamaskController.configManager.getSeedWords() |
||||
assert.equal(getConfigSeed, null) |
||||
}) |
||||
|
||||
}) |
||||
|
||||
describe('#setCurrentLocale', function () { |
||||
|
||||
it('checks the default currentLocale', function () { |
||||
const preferenceCurrentLocale = metamaskController.preferencesController.store.getState().currentLocale |
||||
assert.equal(preferenceCurrentLocale, undefined) |
||||
}) |
||||
|
||||
it('sets current locale in preferences controller', function () { |
||||
metamaskController.setCurrentLocale('ja', noop) |
||||
const preferenceCurrentLocale = metamaskController.preferencesController.store.getState().currentLocale |
||||
assert.equal(preferenceCurrentLocale, 'ja') |
||||
}) |
||||
|
||||
}) |
||||
|
||||
describe('#newUnsignedMessage', function () { |
||||
|
||||
let msgParams, metamaskMsgs, messages, msgId |
||||
|
||||
const address = '0xc42edfcc21ed14dda456aa0756c153f7985d8813' |
||||
const data = '0x43727970746f6b697474696573' |
||||
|
||||
beforeEach(async function () { |
||||
|
||||
await metamaskController.createNewVaultAndRestore('foobar1337', TEST_SEED_ALT) |
||||
|
||||
msgParams = { |
||||
'from': address, |
||||
'data': data, |
||||
} |
||||
|
||||
metamaskController.newUnsignedMessage(msgParams, noop) |
||||
metamaskMsgs = metamaskController.messageManager.getUnapprovedMsgs() |
||||
messages = metamaskController.messageManager.messages |
||||
msgId = Object.keys(metamaskMsgs)[0] |
||||
messages[0].msgParams.metamaskId = parseInt(msgId) |
||||
}) |
||||
|
||||
it('persists address from msg params', function () { |
||||
assert.equal(metamaskMsgs[msgId].msgParams.from, address) |
||||
}) |
||||
|
||||
it('persists data from msg params', function () { |
||||
assert.equal(metamaskMsgs[msgId].msgParams.data, data) |
||||
}) |
||||
|
||||
it('sets the status to unapproved', function () { |
||||
assert.equal(metamaskMsgs[msgId].status, 'unapproved') |
||||
}) |
||||
|
||||
it('sets the type to eth_sign', function () { |
||||
assert.equal(metamaskMsgs[msgId].type, 'eth_sign') |
||||
}) |
||||
|
||||
it('rejects the message', function () { |
||||
const msgIdInt = parseInt(msgId) |
||||
metamaskController.cancelMessage(msgIdInt, noop) |
||||
assert.equal(messages[0].status, 'rejected') |
||||
}) |
||||
|
||||
it('errors when signing a message', async function () { |
||||
try { |
||||
await metamaskController.signMessage(messages[0].msgParams) |
||||
} catch (error) { |
||||
assert.equal(error.message, 'message length is invalid') |
||||
} |
||||
}) |
||||
}) |
||||
|
||||
describe('#newUnsignedPersonalMessage', function () { |
||||
|
||||
it('errors with no from in msgParams', function () { |
||||
const msgParams = { |
||||
'data': data, |
||||
} |
||||
metamaskController.newUnsignedPersonalMessage(msgParams, function (error) { |
||||
assert.equal(error.message, 'MetaMask Message Signature: from field is required.') |
||||
}) |
||||
}) |
||||
|
||||
let msgParams, metamaskPersonalMsgs, personalMessages, msgId |
||||
|
||||
const address = '0xc42edfcc21ed14dda456aa0756c153f7985d8813' |
||||
const data = '0x43727970746f6b697474696573' |
||||
|
||||
beforeEach(async function () { |
||||
|
||||
await metamaskController.createNewVaultAndRestore('foobar1337', TEST_SEED_ALT) |
||||
|
||||
msgParams = { |
||||
'from': address, |
||||
'data': data, |
||||
} |
||||
|
||||
metamaskController.newUnsignedPersonalMessage(msgParams, noop) |
||||
metamaskPersonalMsgs = metamaskController.personalMessageManager.getUnapprovedMsgs() |
||||
personalMessages = metamaskController.personalMessageManager.messages |
||||
msgId = Object.keys(metamaskPersonalMsgs)[0] |
||||
personalMessages[0].msgParams.metamaskId = parseInt(msgId) |
||||
}) |
||||
|
||||
it('persists address from msg params', function () { |
||||
assert.equal(metamaskPersonalMsgs[msgId].msgParams.from, address) |
||||
}) |
||||
|
||||
it('persists data from msg params', function () { |
||||
assert.equal(metamaskPersonalMsgs[msgId].msgParams.data, data) |
||||
}) |
||||
|
||||
it('sets the status to unapproved', function () { |
||||
assert.equal(metamaskPersonalMsgs[msgId].status, 'unapproved') |
||||
}) |
||||
|
||||
it('sets the type to personal_sign', function () { |
||||
assert.equal(metamaskPersonalMsgs[msgId].type, 'personal_sign') |
||||
}) |
||||
|
||||
it('rejects the message', function () { |
||||
const msgIdInt = parseInt(msgId) |
||||
metamaskController.cancelPersonalMessage(msgIdInt, noop) |
||||
assert.equal(personalMessages[0].status, 'rejected') |
||||
}) |
||||
|
||||
it('errors when signing a message', async function () { |
||||
await metamaskController.signPersonalMessage(personalMessages[0].msgParams) |
||||
assert.equal(metamaskPersonalMsgs[msgId].status, 'signed') |
||||
assert.equal(metamaskPersonalMsgs[msgId].rawSig, '0x6a1b65e2b8ed53cf398a769fad24738f9fbe29841fe6854e226953542c4b6a173473cb152b6b1ae5f06d601d45dd699a129b0a8ca84e78b423031db5baa734741b')
|
||||
}) |
||||
}) |
||||
|
||||
describe('#setupUntrustedCommunication', function () { |
||||
let streamTest |
||||
|
||||
const phishingUrl = 'decentral.market' |
||||
|
||||
afterEach(function () { |
||||
streamTest.end() |
||||
}) |
||||
|
||||
it('sets up phishing stream for untrusted communication ', async function () { |
||||
await metamaskController.blacklistController.updatePhishingList() |
||||
|
||||
streamTest = createThoughStream((chunk, enc, cb) => { |
||||
assert.equal(chunk.name, 'phishing') |
||||
assert.equal(chunk.data.hostname, phishingUrl) |
||||
cb() |
||||
}) |
||||
// console.log(streamTest)
|
||||
metamaskController.setupUntrustedCommunication(streamTest, phishingUrl) |
||||
}) |
||||
}) |
||||
|
||||
describe('#setupTrustedCommunication', function () { |
||||
let streamTest |
||||
|
||||
afterEach(function () { |
||||
streamTest.end() |
||||
}) |
||||
|
||||
it('sets up controller dnode api for trusted communication', function (done) { |
||||
streamTest = createThoughStream((chunk, enc, cb) => {
|
||||
assert.equal(chunk.name, 'controller') |
||||
cb() |
||||
done() |
||||
}) |
||||
|
||||
metamaskController.setupTrustedCommunication(streamTest, 'mycrypto.com') |
||||
}) |
||||
}) |
||||
|
||||
describe('#markAccountsFound', function () { |
||||
it('adds lost accounts to config manager data', function () { |
||||
metamaskController.markAccountsFound(noop) |
||||
const configManagerData = metamaskController.configManager.getData() |
||||
assert.deepEqual(configManagerData.lostAccounts, []) |
||||
}) |
||||
}) |
||||
|
||||
describe('#markPasswordForgotten', function () { |
||||
it('adds and sets forgottenPassword to config data to true', function () { |
||||
metamaskController.markPasswordForgotten(noop) |
||||
const configManagerData = metamaskController.configManager.getData() |
||||
assert.equal(configManagerData.forgottenPassword, true) |
||||
}) |
||||
}) |
||||
|
||||
describe('#unMarkPasswordForgotten', function () { |
||||
it('adds and sets forgottenPassword to config data to false', function () { |
||||
metamaskController.unMarkPasswordForgotten(noop) |
||||
const configManagerData = metamaskController.configManager.getData() |
||||
assert.equal(configManagerData.forgottenPassword, false) |
||||
}) |
||||
}) |
||||
|
||||
}) |
@ -1,11 +1,11 @@ |
||||
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') |
||||
|
||||
const { createTestProviderTools } = require('../stub/provider') |
||||
const { createTestProviderTools } = require('../../../stub/provider') |
||||
const providerResultStub = {} |
||||
|
||||
describe('# Network Controller', function () { |
@ -1,6 +1,6 @@ |
||||
const assert = require('assert') |
||||
const configManagerGen = require('../lib/mock-config-manager') |
||||
const NoticeController = require('../../app/scripts/notice-controller') |
||||
const configManagerGen = require('../../../lib/mock-config-manager') |
||||
const NoticeController = require('../../../../app/scripts/notice-controller') |
||||
|
||||
describe('notice-controller', function () { |
||||
var noticeController |
@ -0,0 +1,162 @@ |
||||
const assert = require('assert') |
||||
const PreferencesController = require('../../../../app/scripts/controllers/preferences') |
||||
|
||||
describe('preferences controller', function () { |
||||
let preferencesController |
||||
|
||||
beforeEach(() => { |
||||
preferencesController = new PreferencesController() |
||||
}) |
||||
|
||||
describe('setAddresses', function () { |
||||
it('should keep a map of addresses to names and addresses in the store', function () { |
||||
preferencesController.setAddresses([ |
||||
'0xda22le', |
||||
'0x7e57e2', |
||||
]) |
||||
|
||||
const {identities} = preferencesController.store.getState() |
||||
assert.deepEqual(identities, { |
||||
'0xda22le': { |
||||
name: 'Account 1', |
||||
address: '0xda22le', |
||||
}, |
||||
'0x7e57e2': { |
||||
name: 'Account 2', |
||||
address: '0x7e57e2', |
||||
}, |
||||
}) |
||||
}) |
||||
|
||||
it('should replace its list of addresses', function () { |
||||
preferencesController.setAddresses([ |
||||
'0xda22le', |
||||
'0x7e57e2', |
||||
]) |
||||
preferencesController.setAddresses([ |
||||
'0xda22le77', |
||||
'0x7e57e277', |
||||
]) |
||||
|
||||
const {identities} = preferencesController.store.getState() |
||||
assert.deepEqual(identities, { |
||||
'0xda22le77': { |
||||
name: 'Account 1', |
||||
address: '0xda22le77', |
||||
}, |
||||
'0x7e57e277': { |
||||
name: 'Account 2', |
||||
address: '0x7e57e277', |
||||
}, |
||||
}) |
||||
}) |
||||
}) |
||||
|
||||
describe('setAccountLabel', function () { |
||||
it('should update a label for the given account', function () { |
||||
preferencesController.setAddresses([ |
||||
'0xda22le', |
||||
'0x7e57e2', |
||||
]) |
||||
|
||||
assert.deepEqual(preferencesController.store.getState().identities['0xda22le'], { |
||||
name: 'Account 1', |
||||
address: '0xda22le', |
||||
}) |
||||
|
||||
|
||||
preferencesController.setAccountLabel('0xda22le', 'Dazzle') |
||||
assert.deepEqual(preferencesController.store.getState().identities['0xda22le'], { |
||||
name: 'Dazzle', |
||||
address: '0xda22le', |
||||
}) |
||||
}) |
||||
}) |
||||
|
||||
describe('getTokens', function () { |
||||
it('should return an empty list initially', async function () { |
||||
await preferencesController.setSelectedAddress('0x7e57e2') |
||||
|
||||
const tokens = preferencesController.getTokens() |
||||
assert.equal(tokens.length, 0, 'empty list of tokens') |
||||
}) |
||||
}) |
||||
|
||||
describe('addToken', function () { |
||||
it('should add that token to its state', async function () { |
||||
const address = '0xabcdef1234567' |
||||
const symbol = 'ABBR' |
||||
const decimals = 5 |
||||
|
||||
await preferencesController.setSelectedAddress('0x7e57e2') |
||||
await preferencesController.addToken(address, symbol, decimals) |
||||
|
||||
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') |
||||
}) |
||||
|
||||
it('should allow updating a token value', async function () { |
||||
const address = '0xabcdef1234567' |
||||
const symbol = 'ABBR' |
||||
const decimals = 5 |
||||
|
||||
await preferencesController.setSelectedAddress('0x7e57e2') |
||||
await preferencesController.addToken(address, symbol, decimals) |
||||
|
||||
const newDecimals = 6 |
||||
await preferencesController.addToken(address, symbol, newDecimals) |
||||
|
||||
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, newDecimals, 'updated decimals correctly') |
||||
}) |
||||
|
||||
it('should allow adding tokens to two separate addresses', async function () { |
||||
const address = '0xabcdef1234567' |
||||
const symbol = 'ABBR' |
||||
const decimals = 5 |
||||
|
||||
await preferencesController.setSelectedAddress('0x7e57e2') |
||||
await preferencesController.addToken(address, symbol, decimals) |
||||
assert.equal(preferencesController.getTokens().length, 1, 'one token added for 1st address') |
||||
|
||||
await preferencesController.setSelectedAddress('0xda22le') |
||||
await preferencesController.addToken(address, symbol, decimals) |
||||
assert.equal(preferencesController.getTokens().length, 1, 'one token added for 2nd address') |
||||
}) |
||||
}) |
||||
|
||||
describe('removeToken', function () { |
||||
it('should remove the only token from its state', async function () { |
||||
await preferencesController.setSelectedAddress('0x7e57e2') |
||||
await preferencesController.addToken('0xa', 'A', 5) |
||||
await preferencesController.removeToken('0xa') |
||||
|
||||
const tokens = preferencesController.getTokens() |
||||
assert.equal(tokens.length, 0, 'one token removed') |
||||
}) |
||||
|
||||
it('should remove a token from its state', async function () { |
||||
await preferencesController.setSelectedAddress('0x7e57e2') |
||||
await preferencesController.addToken('0xa', 'A', 4) |
||||
await preferencesController.addToken('0xb', 'B', 5) |
||||
await preferencesController.removeToken('0xa') |
||||
|
||||
const tokens = preferencesController.getTokens() |
||||
assert.equal(tokens.length, 1, 'one token removed') |
||||
|
||||
const [token1] = tokens |
||||
assert.deepEqual(token1, {address: '0xb', symbol: 'B', decimals: 5}) |
||||
}) |
||||
}) |
||||
}) |
||||
|
@ -1,6 +1,6 @@ |
||||
const assert = require('assert') |
||||
const sinon = require('sinon') |
||||
const TokenRatesController = require('../../app/scripts/controllers/token-rates') |
||||
const TokenRatesController = require('../../../../app/scripts/controllers/token-rates') |
||||
const ObservableStore = require('obs-store') |
||||
|
||||
describe('TokenRatesController', () => { |
@ -1,6 +1,6 @@ |
||||
const assert = require('assert') |
||||
const NonceTracker = require('../../app/scripts/controllers/transactions/nonce-tracker') |
||||
const MockTxGen = require('../lib/mock-tx-gen') |
||||
const NonceTracker = require('../../../../../app/scripts/controllers/transactions/nonce-tracker') |
||||
const MockTxGen = require('../../../../lib/mock-tx-gen') |
||||
let providerResultStub = {} |
||||
|
||||
describe('Nonce Tracker', function () { |
@ -1,5 +1,5 @@ |
||||
const assert = require('assert') |
||||
const txHelper = require('../../ui/lib/tx-helper') |
||||
const txHelper = require('../../../../../ui/lib/tx-helper') |
||||
|
||||
describe('txHelper', function () { |
||||
it('always shows the oldest tx first', function () { |
@ -1,6 +1,6 @@ |
||||
const assert = require('assert') |
||||
const txStateHistoryHelper = require('../../app/scripts/controllers/transactions/lib/tx-state-history-helper') |
||||
const testVault = require('../data/v17-long-history.json') |
||||
const txStateHistoryHelper = require('../../../../../app/scripts/controllers/transactions/lib/tx-state-history-helper') |
||||
const testVault = require('../../../../data/v17-long-history.json') |
||||
|
||||
describe ('Transaction state history helper', function () { |
||||
|
@ -1,8 +1,8 @@ |
||||
const assert = require('assert') |
||||
const clone = require('clone') |
||||
const ObservableStore = require('obs-store') |
||||
const TxStateManager = require('../../app/scripts/controllers/transactions/tx-state-manager') |
||||
const txStateHistoryHelper = require('../../app/scripts/controllers/transactions/lib/tx-state-history-helper') |
||||
const TxStateManager = require('../../../../../app/scripts/controllers/transactions/tx-state-manager') |
||||
const txStateHistoryHelper = require('../../../../../app/scripts/controllers/transactions/lib/tx-state-history-helper') |
||||
const noop = () => true |
||||
|
||||
describe('TransactionStateManager', function () { |
@ -1,5 +1,5 @@ |
||||
const assert = require('assert') |
||||
const txUtils = require('../../app/scripts/controllers/transactions/lib/util') |
||||
const txUtils = require('../../../../../app/scripts/controllers/transactions/lib/util') |
||||
|
||||
|
||||
describe('txUtils', function () { |
@ -1,6 +1,6 @@ |
||||
const assert = require('assert') |
||||
|
||||
const EdgeEncryptor = require('../../app/scripts/edge-encryptor') |
||||
const EdgeEncryptor = require('../../../app/scripts/edge-encryptor') |
||||
|
||||
var password = 'passw0rd1' |
||||
var data = 'some random data' |
@ -1,5 +1,5 @@ |
||||
const assert = require('assert') |
||||
const MessageManager = require('../../app/scripts/lib/message-manager') |
||||
const MessageManager = require('../../../app/scripts/lib/message-manager') |
||||
|
||||
describe('Message Manager', function () { |
||||
let messageManager |
@ -1,5 +1,5 @@ |
||||
const assert = require('assert') |
||||
const nodeify = require('../../app/scripts/lib/nodeify') |
||||
const nodeify = require('../../../app/scripts/lib/nodeify') |
||||
|
||||
describe('nodeify', function () { |
||||
var obj = { |
@ -1,6 +1,6 @@ |
||||
const assert = require('assert') |
||||
const PendingBalanceCalculator = require('../../app/scripts/lib/pending-balance-calculator') |
||||
const MockTxGen = require('../lib/mock-tx-gen') |
||||
const PendingBalanceCalculator = require('../../../app/scripts/lib/pending-balance-calculator') |
||||
const MockTxGen = require('../../lib/mock-tx-gen') |
||||
const BN = require('ethereumjs-util').BN |
||||
let providerResultStub = {} |
||||
|
@ -1,6 +1,6 @@ |
||||
const assert = require('assert') |
||||
|
||||
const PersonalMessageManager = require('../../app/scripts/lib/personal-message-manager') |
||||
const PersonalMessageManager = require('../../../app/scripts/lib/personal-message-manager') |
||||
|
||||
describe('Personal Message Manager', function () { |
||||
let messageManager |
@ -1,9 +1,9 @@ |
||||
const assert = require('assert') |
||||
const clone = require('clone') |
||||
const KeyringController = require('eth-keyring-controller') |
||||
const firstTimeState = require('../../app/scripts/first-time-state') |
||||
const seedPhraseVerifier = require('../../app/scripts/lib/seed-phrase-verifier') |
||||
const mockEncryptor = require('../lib/mock-encryptor') |
||||
const firstTimeState = require('../../../app/scripts/first-time-state') |
||||
const seedPhraseVerifier = require('../../../app/scripts/lib/seed-phrase-verifier') |
||||
const mockEncryptor = require('../../lib/mock-encryptor') |
||||
|
||||
describe('SeedPhraseVerifier', function () { |
||||
|
@ -1,5 +1,5 @@ |
||||
const assert = require('assert') |
||||
const { sufficientBalance } = require('../../app/scripts/lib/util') |
||||
const { sufficientBalance } = require('../../../app/scripts/lib/util') |
||||
|
||||
|
||||
describe('SufficientBalance', function () { |
@ -1,120 +0,0 @@ |
||||
const assert = require('assert') |
||||
const sinon = require('sinon') |
||||
const clone = require('clone') |
||||
const nock = require('nock') |
||||
const MetaMaskController = require('../../app/scripts/metamask-controller') |
||||
const blacklistJSON = require('../stub/blacklist') |
||||
const firstTimeState = require('../../app/scripts/first-time-state') |
||||
|
||||
const DEFAULT_LABEL = 'Account 1' |
||||
const TEST_SEED = 'debris dizzy just program just float decrease vacant alarm reduce speak stadium' |
||||
const TEST_ADDRESS = '0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc' |
||||
const TEST_SEED_ALT = 'setup olympic issue mobile velvet surge alcohol burger horse view reopen gentle' |
||||
const TEST_ADDRESS_ALT = '0xc42edfcc21ed14dda456aa0756c153f7985d8813' |
||||
|
||||
describe('MetaMaskController', function () { |
||||
let metamaskController |
||||
const sandbox = sinon.sandbox.create() |
||||
const noop = () => { } |
||||
|
||||
beforeEach(function () { |
||||
|
||||
nock('https://api.infura.io') |
||||
.persist() |
||||
.get('/v2/blacklist') |
||||
.reply(200, blacklistJSON) |
||||
|
||||
nock('https://api.infura.io') |
||||
.persist() |
||||
.get(/.*/) |
||||
.reply(200) |
||||
|
||||
metamaskController = new MetaMaskController({ |
||||
showUnapprovedTx: noop, |
||||
encryptor: { |
||||
encrypt: function (password, object) { |
||||
this.object = object |
||||
return Promise.resolve() |
||||
}, |
||||
decrypt: function () { |
||||
return Promise.resolve(this.object) |
||||
}, |
||||
}, |
||||
initState: clone(firstTimeState), |
||||
}) |
||||
sandbox.spy(metamaskController.keyringController, 'createNewVaultAndKeychain') |
||||
sandbox.spy(metamaskController.keyringController, 'createNewVaultAndRestore') |
||||
}) |
||||
|
||||
afterEach(function () { |
||||
nock.cleanAll() |
||||
sandbox.restore() |
||||
}) |
||||
|
||||
describe('#getGasPrice', function () { |
||||
it('gives the 50th percentile lowest accepted gas price from recentBlocksController', async function () { |
||||
const realRecentBlocksController = metamaskController.recentBlocksController |
||||
metamaskController.recentBlocksController = { |
||||
store: { |
||||
getState: () => { |
||||
return { |
||||
recentBlocks: [ |
||||
{ gasPrices: [ '0x3b9aca00', '0x174876e800'] }, |
||||
{ gasPrices: [ '0x3b9aca00', '0x174876e800'] }, |
||||
{ gasPrices: [ '0x174876e800', '0x174876e800' ]}, |
||||
{ gasPrices: [ '0x174876e800', '0x174876e800' ]}, |
||||
], |
||||
} |
||||
}, |
||||
}, |
||||
} |
||||
|
||||
const gasPrice = metamaskController.getGasPrice() |
||||
assert.equal(gasPrice, '0x3b9aca00', 'accurately estimates 50th percentile accepted gas price') |
||||
|
||||
metamaskController.recentBlocksController = realRecentBlocksController |
||||
}) |
||||
}) |
||||
|
||||
describe('#createNewVaultAndKeychain', function () { |
||||
it('can only create new vault on keyringController once', async function () { |
||||
const selectStub = sandbox.stub(metamaskController, 'selectFirstIdentity') |
||||
|
||||
const password = 'a-fake-password' |
||||
|
||||
await metamaskController.createNewVaultAndKeychain(password) |
||||
await metamaskController.createNewVaultAndKeychain(password) |
||||
|
||||
assert(metamaskController.keyringController.createNewVaultAndKeychain.calledOnce) |
||||
|
||||
selectStub.reset() |
||||
}) |
||||
}) |
||||
|
||||
describe('#createNewVaultAndRestore', function () { |
||||
it('should be able to call newVaultAndRestore despite a mistake.', async function () { |
||||
const password = 'what-what-what' |
||||
await metamaskController.createNewVaultAndRestore(password, TEST_SEED.slice(0, -1)).catch((e) => null) |
||||
await metamaskController.createNewVaultAndRestore(password, TEST_SEED) |
||||
|
||||
assert(metamaskController.keyringController.createNewVaultAndRestore.calledTwice) |
||||
}) |
||||
|
||||
it('should clear previous identities after vault restoration', async () => { |
||||
await metamaskController.createNewVaultAndRestore('foobar1337', TEST_SEED) |
||||
assert.deepEqual(metamaskController.getState().identities, { |
||||
[TEST_ADDRESS]: { address: TEST_ADDRESS, name: DEFAULT_LABEL }, |
||||
}) |
||||
|
||||
await metamaskController.keyringController.saveAccountLabel(TEST_ADDRESS, 'Account Foo') |
||||
assert.deepEqual(metamaskController.getState().identities, { |
||||
[TEST_ADDRESS]: { address: TEST_ADDRESS, name: 'Account Foo' }, |
||||
}) |
||||
|
||||
await metamaskController.createNewVaultAndRestore('foobar1337', TEST_SEED_ALT) |
||||
assert.deepEqual(metamaskController.getState().identities, { |
||||
[TEST_ADDRESS_ALT]: { address: TEST_ADDRESS_ALT, name: DEFAULT_LABEL }, |
||||
}) |
||||
}) |
||||
}) |
||||
}) |
@ -0,0 +1,41 @@ |
||||
const assert = require('assert') |
||||
const migration26 = require('../../../app/scripts/migrations/026') |
||||
const oldStorage = { |
||||
'meta': {'version': 25}, |
||||
'data': { |
||||
'PreferencesController': {}, |
||||
'KeyringController': { |
||||
'walletNicknames': { |
||||
'0x1e77e2': 'Test Account 1', |
||||
'0x7e57e2': 'Test Account 2', |
||||
}, |
||||
}, |
||||
}, |
||||
} |
||||
|
||||
describe('migration #26', () => { |
||||
it('should move the identities from KeyringController', (done) => { |
||||
migration26.migrate(oldStorage) |
||||
.then((newStorage) => { |
||||
const identities = newStorage.data.PreferencesController.identities |
||||
assert.deepEqual(identities, { |
||||
'0x1e77e2': {name: 'Test Account 1', address: '0x1e77e2'}, |
||||
'0x7e57e2': {name: 'Test Account 2', address: '0x7e57e2'}, |
||||
}) |
||||
assert.strictEqual(newStorage.data.KeyringController.walletNicknames, undefined) |
||||
done() |
||||
}) |
||||
.catch(done) |
||||
}) |
||||
|
||||
it('should successfully migrate first time state', (done) => { |
||||
migration26.migrate({ |
||||
meta: {}, |
||||
data: require('../../../app/scripts/first-time-state'), |
||||
}) |
||||
.then((migratedData) => { |
||||
assert.equal(migratedData.meta.version, migration26.version) |
||||
done() |
||||
}).catch(done) |
||||
}) |
||||
}) |
@ -1,22 +1,22 @@ |
||||
const assert = require('assert') |
||||
const path = require('path') |
||||
|
||||
const wallet1 = require(path.join('..', 'lib', 'migrations', '001.json')) |
||||
const vault4 = require(path.join('..', 'lib', 'migrations', '004.json')) |
||||
const wallet1 = require(path.join('..', '..', 'lib', 'migrations', '001.json')) |
||||
const vault4 = require(path.join('..', '..', 'lib', 'migrations', '004.json')) |
||||
let vault5, vault6, vault7, vault8, vault9 // vault10, vault11
|
||||
|
||||
const migration2 = require(path.join('..', '..', 'app', 'scripts', 'migrations', '002')) |
||||
const migration3 = require(path.join('..', '..', 'app', 'scripts', 'migrations', '003')) |
||||
const migration4 = require(path.join('..', '..', 'app', 'scripts', 'migrations', '004')) |
||||
const migration5 = require(path.join('..', '..', 'app', 'scripts', 'migrations', '005')) |
||||
const migration6 = require(path.join('..', '..', 'app', 'scripts', 'migrations', '006')) |
||||
const migration7 = require(path.join('..', '..', 'app', 'scripts', 'migrations', '007')) |
||||
const migration8 = require(path.join('..', '..', 'app', 'scripts', 'migrations', '008')) |
||||
const migration9 = require(path.join('..', '..', 'app', 'scripts', 'migrations', '009')) |
||||
const migration10 = require(path.join('..', '..', 'app', 'scripts', 'migrations', '010')) |
||||
const migration11 = require(path.join('..', '..', 'app', 'scripts', 'migrations', '011')) |
||||
const migration12 = require(path.join('..', '..', 'app', 'scripts', 'migrations', '012')) |
||||
const migration13 = require(path.join('..', '..', 'app', 'scripts', 'migrations', '013')) |
||||
const migration2 = require(path.join('..', '..', '..', 'app', 'scripts', 'migrations', '002')) |
||||
const migration3 = require(path.join('..', '..', '..', 'app', 'scripts', 'migrations', '003')) |
||||
const migration4 = require(path.join('..', '..', '..', 'app', 'scripts', 'migrations', '004')) |
||||
const migration5 = require(path.join('..', '..', '..', 'app', 'scripts', 'migrations', '005')) |
||||
const migration6 = require(path.join('..', '..', '..', 'app', 'scripts', 'migrations', '006')) |
||||
const migration7 = require(path.join('..', '..', '..', 'app', 'scripts', 'migrations', '007')) |
||||
const migration8 = require(path.join('..', '..', '..', 'app', 'scripts', 'migrations', '008')) |
||||
const migration9 = require(path.join('..', '..', '..', 'app', 'scripts', 'migrations', '009')) |
||||
const migration10 = require(path.join('..', '..', '..', 'app', 'scripts', 'migrations', '010')) |
||||
const migration11 = require(path.join('..', '..', '..', 'app', 'scripts', 'migrations', '011')) |
||||
const migration12 = require(path.join('..', '..', '..', 'app', 'scripts', 'migrations', '012')) |
||||
const migration13 = require(path.join('..', '..', '..', 'app', 'scripts', 'migrations', '013')) |
||||
|
||||
|
||||
const oldTestRpc = 'https://rawtestrpc.metamask.io/' |
@ -1,48 +0,0 @@ |
||||
const assert = require('assert') |
||||
const PreferencesController = require('../../app/scripts/controllers/preferences') |
||||
|
||||
describe('preferences controller', function () { |
||||
let preferencesController |
||||
|
||||
before(() => { |
||||
preferencesController = new PreferencesController() |
||||
}) |
||||
|
||||
describe('addToken', function () { |
||||
it('should add that token to its state', async function () { |
||||
const address = '0xabcdef1234567' |
||||
const symbol = 'ABBR' |
||||
const decimals = 5 |
||||
|
||||
await preferencesController.addToken(address, symbol, decimals) |
||||
|
||||
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') |
||||
}) |
||||
|
||||
it('should allow updating a token value', async function () { |
||||
const address = '0xabcdef1234567' |
||||
const symbol = 'ABBR' |
||||
const decimals = 5 |
||||
|
||||
await preferencesController.addToken(address, symbol, decimals) |
||||
|
||||
const newDecimals = 6 |
||||
await preferencesController.addToken(address, symbol, newDecimals) |
||||
|
||||
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, newDecimals, 'updated decimals correctly') |
||||
}) |
||||
}) |
||||
}) |
||||
|
@ -1,2 +1,2 @@ |
||||
const Button = require('./button.component') |
||||
import Button from './button.component' |
||||
module.exports = Button |
||||
|
@ -0,0 +1,5 @@ |
||||
@import './export-text-container/index'; |
||||
|
||||
@import './info-box/index'; |
||||
|
||||
@import './pages/index'; |
@ -0,0 +1,2 @@ |
||||
import InfoBox from './info-box.component' |
||||
module.exports = InfoBox |
@ -0,0 +1,24 @@ |
||||
.info-box { |
||||
border-radius: 4px; |
||||
background-color: $alabaster; |
||||
position: relative; |
||||
padding: 16px; |
||||
display: flex; |
||||
flex-flow: column; |
||||
color: $mid-gray; |
||||
|
||||
&__close::after { |
||||
content: '\00D7'; |
||||
font-size: 29px; |
||||
font-weight: 200; |
||||
color: $dusty-gray; |
||||
position: absolute; |
||||
right: 12px; |
||||
top: 0; |
||||
cursor: pointer; |
||||
} |
||||
|
||||
&__description { |
||||
font-size: .75rem; |
||||
} |
||||
} |
@ -0,0 +1,49 @@ |
||||
import React, { Component } from 'react' |
||||
import PropTypes from 'prop-types' |
||||
|
||||
export default class InfoBox extends Component { |
||||
static contextTypes = { |
||||
t: PropTypes.func, |
||||
} |
||||
|
||||
static propTypes = { |
||||
onClose: PropTypes.func, |
||||
title: PropTypes.string, |
||||
description: PropTypes.string, |
||||
} |
||||
|
||||
constructor (props) { |
||||
super(props) |
||||
|
||||
this.state = { |
||||
isShowing: true, |
||||
} |
||||
} |
||||
|
||||
handleClose () { |
||||
const { onClose } = this.props |
||||
|
||||
if (onClose) { |
||||
onClose() |
||||
} else { |
||||
this.setState({ isShowing: false }) |
||||
} |
||||
} |
||||
|
||||
render () { |
||||
const { title, description } = this.props |
||||
|
||||
return !this.state.isShowing |
||||
? null |
||||
: ( |
||||
<div className="info-box"> |
||||
<div |
||||
className="info-box__close" |
||||
onClick={() => this.handleClose()} |
||||
/> |
||||
<div className="info-box__title">{ title }</div> |
||||
<div className="info-box__description">{ description }</div> |
||||
</div> |
||||
) |
||||
} |
||||
} |
@ -1,431 +0,0 @@ |
||||
const inherits = require('util').inherits |
||||
const Component = require('react').Component |
||||
const classnames = require('classnames') |
||||
const h = require('react-hyperscript') |
||||
const PropTypes = require('prop-types') |
||||
const connect = require('react-redux').connect |
||||
const R = require('ramda') |
||||
const Fuse = require('fuse.js') |
||||
const contractMap = require('eth-contract-metadata') |
||||
const TokenBalance = require('../../components/token-balance') |
||||
const Identicon = require('../../components/identicon') |
||||
const contractList = Object.entries(contractMap) |
||||
.map(([ _, tokenData]) => tokenData) |
||||
.filter(tokenData => Boolean(tokenData.erc20)) |
||||
const fuse = new Fuse(contractList, { |
||||
shouldSort: true, |
||||
threshold: 0.45, |
||||
location: 0, |
||||
distance: 100, |
||||
maxPatternLength: 32, |
||||
minMatchCharLength: 1, |
||||
keys: [ |
||||
{ name: 'name', weight: 0.5 }, |
||||
{ name: 'symbol', weight: 0.5 }, |
||||
], |
||||
}) |
||||
const actions = require('../../actions') |
||||
const ethUtil = require('ethereumjs-util') |
||||
const { tokenInfoGetter } = require('../../token-util') |
||||
const { DEFAULT_ROUTE } = require('../../routes') |
||||
|
||||
const emptyAddr = '0x0000000000000000000000000000000000000000' |
||||
|
||||
AddTokenScreen.contextTypes = { |
||||
t: PropTypes.func, |
||||
} |
||||
|
||||
module.exports = connect(mapStateToProps, mapDispatchToProps)(AddTokenScreen) |
||||
|
||||
|
||||
function mapStateToProps (state) { |
||||
const { identities, tokens } = state.metamask |
||||
return { |
||||
identities, |
||||
tokens, |
||||
} |
||||
} |
||||
|
||||
function mapDispatchToProps (dispatch) { |
||||
return { |
||||
addTokens: tokens => dispatch(actions.addTokens(tokens)), |
||||
} |
||||
} |
||||
|
||||
inherits(AddTokenScreen, Component) |
||||
function AddTokenScreen () { |
||||
this.state = { |
||||
isShowingConfirmation: false, |
||||
isShowingInfoBox: true, |
||||
customAddress: '', |
||||
customSymbol: '', |
||||
customDecimals: '', |
||||
searchQuery: '', |
||||
selectedTokens: {}, |
||||
errors: {}, |
||||
autoFilled: false, |
||||
displayedTab: 'SEARCH', |
||||
} |
||||
this.tokenAddressDidChange = this.tokenAddressDidChange.bind(this) |
||||
this.tokenSymbolDidChange = this.tokenSymbolDidChange.bind(this) |
||||
this.tokenDecimalsDidChange = this.tokenDecimalsDidChange.bind(this) |
||||
this.onNext = this.onNext.bind(this) |
||||
Component.call(this) |
||||
} |
||||
|
||||
AddTokenScreen.prototype.componentWillMount = function () { |
||||
this.tokenInfoGetter = tokenInfoGetter() |
||||
} |
||||
|
||||
AddTokenScreen.prototype.toggleToken = function (address, token) { |
||||
const { selectedTokens = {}, errors } = this.state |
||||
const selectedTokensCopy = { ...selectedTokens } |
||||
|
||||
if (address in selectedTokensCopy) { |
||||
delete selectedTokensCopy[address] |
||||
} else { |
||||
selectedTokensCopy[address] = token |
||||
} |
||||
|
||||
this.setState({ |
||||
selectedTokens: selectedTokensCopy, |
||||
errors: { |
||||
...errors, |
||||
tokenSelector: null, |
||||
}, |
||||
}) |
||||
} |
||||
|
||||
AddTokenScreen.prototype.onNext = function () { |
||||
const { isValid, errors } = this.validate() |
||||
|
||||
return !isValid |
||||
? this.setState({ errors }) |
||||
: this.setState({ isShowingConfirmation: true }) |
||||
} |
||||
|
||||
AddTokenScreen.prototype.tokenAddressDidChange = function (e) { |
||||
const customAddress = e.target.value.trim() |
||||
this.setState({ customAddress }) |
||||
if (ethUtil.isValidAddress(customAddress) && customAddress !== emptyAddr) { |
||||
this.attemptToAutoFillTokenParams(customAddress) |
||||
} else { |
||||
this.setState({ |
||||
customSymbol: '', |
||||
customDecimals: 0, |
||||
}) |
||||
} |
||||
} |
||||
|
||||
AddTokenScreen.prototype.tokenSymbolDidChange = function (e) { |
||||
const customSymbol = e.target.value.trim() |
||||
this.setState({ customSymbol }) |
||||
} |
||||
|
||||
AddTokenScreen.prototype.tokenDecimalsDidChange = function (e) { |
||||
const customDecimals = e.target.value.trim() |
||||
this.setState({ customDecimals }) |
||||
} |
||||
|
||||
AddTokenScreen.prototype.checkExistingAddresses = function (address) { |
||||
if (!address) return false |
||||
const tokensList = this.props.tokens |
||||
const matchesAddress = existingToken => { |
||||
return existingToken.address.toLowerCase() === address.toLowerCase() |
||||
} |
||||
|
||||
return R.any(matchesAddress)(tokensList) |
||||
} |
||||
|
||||
AddTokenScreen.prototype.validate = function () { |
||||
const errors = {} |
||||
const identitiesList = Object.keys(this.props.identities) |
||||
const { customAddress, customSymbol, customDecimals, selectedTokens } = this.state |
||||
const standardAddress = ethUtil.addHexPrefix(customAddress).toLowerCase() |
||||
|
||||
if (customAddress) { |
||||
const validAddress = ethUtil.isValidAddress(customAddress) |
||||
if (!validAddress) { |
||||
errors.customAddress = this.context.t('invalidAddress') |
||||
} |
||||
|
||||
const validDecimals = customDecimals !== null |
||||
&& customDecimals !== '' |
||||
&& customDecimals >= 0 |
||||
&& customDecimals < 36 |
||||
if (!validDecimals) { |
||||
errors.customDecimals = this.context.t('decimalsMustZerotoTen') |
||||
} |
||||
|
||||
const symbolLen = customSymbol.trim().length |
||||
const validSymbol = symbolLen > 0 && symbolLen < 10 |
||||
if (!validSymbol) { |
||||
errors.customSymbol = this.context.t('symbolBetweenZeroTen') |
||||
} |
||||
|
||||
const ownAddress = identitiesList.includes(standardAddress) |
||||
if (ownAddress) { |
||||
errors.customAddress = this.context.t('personalAddressDetected') |
||||
} |
||||
|
||||
const tokenAlreadyAdded = this.checkExistingAddresses(customAddress) |
||||
if (tokenAlreadyAdded) { |
||||
errors.customAddress = this.context.t('tokenAlreadyAdded') |
||||
} |
||||
} else if ( |
||||
Object.entries(selectedTokens) |
||||
.reduce((isEmpty, [ symbol, isSelected ]) => ( |
||||
isEmpty && !isSelected |
||||
), true) |
||||
) { |
||||
errors.tokenSelector = this.context.t('mustSelectOne') |
||||
} |
||||
|
||||
return { |
||||
isValid: !Object.keys(errors).length, |
||||
errors, |
||||
} |
||||
} |
||||
|
||||
AddTokenScreen.prototype.attemptToAutoFillTokenParams = async function (address) { |
||||
const { symbol, decimals } = await this.tokenInfoGetter(address) |
||||
if (symbol && decimals) { |
||||
this.setState({ |
||||
customSymbol: symbol, |
||||
customDecimals: decimals, |
||||
autoFilled: true, |
||||
}) |
||||
} |
||||
} |
||||
|
||||
AddTokenScreen.prototype.renderCustomForm = function () { |
||||
const { autoFilled, customAddress, customSymbol, customDecimals, errors } = this.state |
||||
|
||||
return ( |
||||
h('div.add-token__add-custom-form', [ |
||||
h('div', { |
||||
className: classnames('add-token__add-custom-field', { |
||||
'add-token__add-custom-field--error': errors.customAddress, |
||||
}), |
||||
}, [ |
||||
h('div.add-token__add-custom-label', this.context.t('tokenAddress')), |
||||
h('input.add-token__add-custom-input', { |
||||
type: 'text', |
||||
onChange: this.tokenAddressDidChange, |
||||
value: customAddress, |
||||
}), |
||||
h('div.add-token__add-custom-error-message', errors.customAddress), |
||||
]), |
||||
h('div', { |
||||
className: classnames('add-token__add-custom-field', { |
||||
'add-token__add-custom-field--error': errors.customSymbol, |
||||
}), |
||||
}, [ |
||||
h('div.add-token__add-custom-label', this.context.t('tokenSymbol')), |
||||
h('input.add-token__add-custom-input', { |
||||
type: 'text', |
||||
onChange: this.tokenSymbolDidChange, |
||||
value: customSymbol, |
||||
disabled: autoFilled, |
||||
}), |
||||
h('div.add-token__add-custom-error-message', errors.customSymbol), |
||||
]), |
||||
h('div', { |
||||
className: classnames('add-token__add-custom-field', { |
||||
'add-token__add-custom-field--error': errors.customDecimals, |
||||
}), |
||||
}, [ |
||||
h('div.add-token__add-custom-label', this.context.t('decimal')), |
||||
h('input.add-token__add-custom-input', { |
||||
type: 'number', |
||||
onChange: this.tokenDecimalsDidChange, |
||||
value: customDecimals, |
||||
disabled: autoFilled, |
||||
}), |
||||
h('div.add-token__add-custom-error-message', errors.customDecimals), |
||||
]), |
||||
]) |
||||
) |
||||
} |
||||
|
||||
AddTokenScreen.prototype.renderTokenList = function () { |
||||
const { searchQuery = '', selectedTokens } = this.state |
||||
const fuseSearchResult = fuse.search(searchQuery) |
||||
const addressSearchResult = contractList.filter(token => { |
||||
return token.address.toLowerCase() === searchQuery.toLowerCase() |
||||
}) |
||||
const results = [...addressSearchResult, ...fuseSearchResult] |
||||
|
||||
return h('div', [ |
||||
results.length > 0 && h('div.add-token__token-icons-title', this.context.t('popularTokens')), |
||||
h('div.add-token__token-icons-container', Array(6).fill(undefined) |
||||
.map((_, i) => { |
||||
const { logo, symbol, name, address } = results[i] || {} |
||||
const tokenAlreadyAdded = this.checkExistingAddresses(address) |
||||
return Boolean(logo || symbol || name) && ( |
||||
h('div.add-token__token-wrapper', { |
||||
className: classnames({ |
||||
'add-token__token-wrapper--selected': selectedTokens[address], |
||||
'add-token__token-wrapper--disabled': tokenAlreadyAdded, |
||||
}), |
||||
onClick: () => !tokenAlreadyAdded && this.toggleToken(address, results[i]), |
||||
}, [ |
||||
h('div.add-token__token-icon', { |
||||
style: { |
||||
backgroundImage: logo && `url(images/contract/${logo})`, |
||||
}, |
||||
}), |
||||
h('div.add-token__token-data', [ |
||||
h('div.add-token__token-symbol', symbol), |
||||
h('div.add-token__token-name', name), |
||||
]), |
||||
// tokenAlreadyAdded && (
|
||||
// h('div.add-token__token-message', 'Already added')
|
||||
// ),
|
||||
]) |
||||
) |
||||
})), |
||||
]) |
||||
} |
||||
|
||||
AddTokenScreen.prototype.renderConfirmation = function () { |
||||
const { |
||||
customAddress: address, |
||||
customSymbol: symbol, |
||||
customDecimals: decimals, |
||||
selectedTokens, |
||||
} = this.state |
||||
|
||||
const { addTokens, history } = this.props |
||||
|
||||
const customToken = { |
||||
address, |
||||
symbol, |
||||
decimals, |
||||
} |
||||
|
||||
const tokens = address && symbol && decimals |
||||
? { ...selectedTokens, [address]: customToken } |
||||
: selectedTokens |
||||
|
||||
return ( |
||||
h('div.add-token', [ |
||||
h('div.add-token__wrapper', [ |
||||
h('div.add-token__content-container.add-token__confirmation-content', [ |
||||
h('div.add-token__description.add-token__confirmation-description', this.context.t('balances')), |
||||
h('div.add-token__confirmation-token-list', |
||||
Object.entries(tokens) |
||||
.map(([ address, token ]) => ( |
||||
h('span.add-token__confirmation-token-list-item', [ |
||||
h(Identicon, { |
||||
className: 'add-token__confirmation-token-icon', |
||||
diameter: 75, |
||||
address, |
||||
}), |
||||
h(TokenBalance, { token }), |
||||
]) |
||||
)) |
||||
), |
||||
]), |
||||
]), |
||||
h('div.add-token__buttons', [ |
||||
h('button.btn-secondary--lg.add-token__cancel-button', { |
||||
onClick: () => this.setState({ isShowingConfirmation: false }), |
||||
}, this.context.t('back')), |
||||
h('button.btn-primary--lg', { |
||||
onClick: () => addTokens(tokens).then(() => history.push(DEFAULT_ROUTE)), |
||||
}, this.context.t('addTokens')), |
||||
]), |
||||
]) |
||||
) |
||||
} |
||||
|
||||
AddTokenScreen.prototype.displayTab = function (selectedTab) { |
||||
this.setState({ displayedTab: selectedTab }) |
||||
} |
||||
|
||||
AddTokenScreen.prototype.renderTabs = function () { |
||||
const { isShowingInfoBox, displayedTab, errors } = this.state |
||||
|
||||
return displayedTab === 'CUSTOM_TOKEN' |
||||
? this.renderCustomForm() |
||||
: h('div', [ |
||||
h('div.add-token__wrapper', [ |
||||
h('div.add-token__content-container', [ |
||||
isShowingInfoBox && h('div.add-token__info-box', [ |
||||
h('div.add-token__info-box__close', { |
||||
onClick: () => this.setState({ isShowingInfoBox: false }), |
||||
}), |
||||
h('div.add-token__info-box__title', this.context.t('whatsThis')), |
||||
h('div.add-token__info-box__copy', this.context.t('keepTrackTokens')), |
||||
h('a.add-token__info-box__copy--blue', { |
||||
href: 'http://metamask.helpscoutdocs.com/article/16-managing-erc20-tokens', |
||||
target: '_blank', |
||||
}, this.context.t('learnMore')), |
||||
]), |
||||
h('div.add-token__input-container', [ |
||||
h('input.add-token__input', { |
||||
type: 'text', |
||||
placeholder: this.context.t('searchTokens'), |
||||
onChange: e => this.setState({ searchQuery: e.target.value }), |
||||
}), |
||||
h('div.add-token__search-input-error-message', errors.tokenSelector), |
||||
]), |
||||
this.renderTokenList(), |
||||
]), |
||||
]), |
||||
]) |
||||
} |
||||
|
||||
AddTokenScreen.prototype.render = function () { |
||||
const { |
||||
isShowingConfirmation, |
||||
displayedTab, |
||||
} = this.state |
||||
const { history } = this.props |
||||
|
||||
return h('div.add-token', [ |
||||
h('div.add-token__header', [ |
||||
h('div.add-token__header__cancel', { |
||||
onClick: () => history.push(DEFAULT_ROUTE), |
||||
}, [ |
||||
h('i.fa.fa-angle-left.fa-lg'), |
||||
h('span', this.context.t('cancel')), |
||||
]), |
||||
h('div.add-token__header__title', this.context.t('addTokens')), |
||||
isShowingConfirmation && h('div.add-token__header__subtitle', this.context.t('likeToAddTokens')), |
||||
!isShowingConfirmation && h('div.add-token__header__tabs', [ |
||||
|
||||
h('div.add-token__header__tabs__tab', { |
||||
className: classnames('add-token__header__tabs__tab', { |
||||
'add-token__header__tabs__selected': displayedTab === 'SEARCH', |
||||
'add-token__header__tabs__unselected': displayedTab !== 'SEARCH', |
||||
}), |
||||
onClick: () => this.displayTab('SEARCH'), |
||||
}, this.context.t('search')), |
||||
|
||||
h('div.add-token__header__tabs__tab', { |
||||
className: classnames('add-token__header__tabs__tab', { |
||||
'add-token__header__tabs__selected': displayedTab === 'CUSTOM_TOKEN', |
||||
'add-token__header__tabs__unselected': displayedTab !== 'CUSTOM_TOKEN', |
||||
}), |
||||
onClick: () => this.displayTab('CUSTOM_TOKEN'), |
||||
}, this.context.t('customToken')), |
||||
|
||||
]), |
||||
]), |
||||
|
||||
isShowingConfirmation |
||||
? this.renderConfirmation() |
||||
: this.renderTabs(), |
||||
|
||||
!isShowingConfirmation && h('div.add-token__buttons', [ |
||||
h('button.btn-secondary--lg.add-token__cancel-button', { |
||||
onClick: () => history.push(DEFAULT_ROUTE), |
||||
}, this.context.t('cancel')), |
||||
h('button.btn-primary--lg.add-token__confirm-button', { |
||||
onClick: this.onNext, |
||||
}, this.context.t('next')), |
||||
]), |
||||
]) |
||||
} |
@ -0,0 +1,351 @@ |
||||
import React, { Component } from 'react' |
||||
import classnames from 'classnames' |
||||
import PropTypes from 'prop-types' |
||||
import ethUtil from 'ethereumjs-util' |
||||
import { checkExistingAddresses } from './util' |
||||
import { tokenInfoGetter } from '../../../token-util' |
||||
import { DEFAULT_ROUTE, CONFIRM_ADD_TOKEN_ROUTE } from '../../../routes' |
||||
import Button from '../../button' |
||||
import TextField from '../../text-field' |
||||
import TokenList from './token-list' |
||||
import TokenSearch from './token-search' |
||||
|
||||
const emptyAddr = '0x0000000000000000000000000000000000000000' |
||||
const SEARCH_TAB = 'SEARCH' |
||||
const CUSTOM_TOKEN_TAB = 'CUSTOM_TOKEN' |
||||
|
||||
class AddToken extends Component { |
||||
static contextTypes = { |
||||
t: PropTypes.func, |
||||
} |
||||
|
||||
static propTypes = { |
||||
history: PropTypes.object, |
||||
setPendingTokens: PropTypes.func, |
||||
pendingTokens: PropTypes.object, |
||||
clearPendingTokens: PropTypes.func, |
||||
tokens: PropTypes.array, |
||||
identities: PropTypes.object, |
||||
} |
||||
|
||||
constructor (props) { |
||||
super(props) |
||||
|
||||
this.state = { |
||||
customAddress: '', |
||||
customSymbol: '', |
||||
customDecimals: 0, |
||||
searchResults: [], |
||||
selectedTokens: {}, |
||||
tokenSelectorError: null, |
||||
customAddressError: null, |
||||
customSymbolError: null, |
||||
customDecimalsError: null, |
||||
autoFilled: false, |
||||
displayedTab: SEARCH_TAB, |
||||
} |
||||
} |
||||
|
||||
componentDidMount () { |
||||
this.tokenInfoGetter = tokenInfoGetter() |
||||
const { pendingTokens = {} } = this.props |
||||
const pendingTokenKeys = Object.keys(pendingTokens) |
||||
|
||||
if (pendingTokenKeys.length > 0) { |
||||
let selectedTokens = {} |
||||
let customToken = {} |
||||
|
||||
pendingTokenKeys.forEach(tokenAddress => { |
||||
const token = pendingTokens[tokenAddress] |
||||
const { isCustom } = token |
||||
|
||||
if (isCustom) { |
||||
customToken = { ...token } |
||||
} else { |
||||
selectedTokens = { ...selectedTokens, [tokenAddress]: { ...token } } |
||||
} |
||||
}) |
||||
|
||||
const { |
||||
address: customAddress = '', |
||||
symbol: customSymbol = '', |
||||
decimals: customDecimals = 0, |
||||
} = customToken |
||||
|
||||
const displayedTab = Object.keys(selectedTokens).length > 0 ? SEARCH_TAB : CUSTOM_TOKEN_TAB |
||||
this.setState({ selectedTokens, customAddress, customSymbol, customDecimals, displayedTab }) |
||||
} |
||||
} |
||||
|
||||
handleToggleToken (token) { |
||||
const { address } = token |
||||
const { selectedTokens = {} } = this.state |
||||
const selectedTokensCopy = { ...selectedTokens } |
||||
|
||||
if (address in selectedTokensCopy) { |
||||
delete selectedTokensCopy[address] |
||||
} else { |
||||
selectedTokensCopy[address] = token |
||||
} |
||||
|
||||
this.setState({ |
||||
selectedTokens: selectedTokensCopy, |
||||
tokenSelectorError: null, |
||||
}) |
||||
} |
||||
|
||||
hasError () { |
||||
const { |
||||
tokenSelectorError, |
||||
customAddressError, |
||||
customSymbolError, |
||||
customDecimalsError, |
||||
} = this.state |
||||
|
||||
return tokenSelectorError || customAddressError || customSymbolError || customDecimalsError |
||||
} |
||||
|
||||
hasSelected () { |
||||
const { customAddress = '', selectedTokens = {} } = this.state |
||||
return customAddress || Object.keys(selectedTokens).length > 0 |
||||
} |
||||
|
||||
handleNext () { |
||||
if (this.hasError()) { |
||||
return |
||||
} |
||||
|
||||
if (!this.hasSelected()) { |
||||
this.setState({ tokenSelectorError: this.context.t('mustSelectOne') }) |
||||
return |
||||
} |
||||
|
||||
const { setPendingTokens, history } = this.props |
||||
const { |
||||
customAddress: address, |
||||
customSymbol: symbol, |
||||
customDecimals: decimals, |
||||
selectedTokens, |
||||
} = this.state |
||||
|
||||
const customToken = { |
||||
address, |
||||
symbol, |
||||
decimals, |
||||
} |
||||
|
||||
setPendingTokens({ customToken, selectedTokens }) |
||||
history.push(CONFIRM_ADD_TOKEN_ROUTE) |
||||
} |
||||
|
||||
async attemptToAutoFillTokenParams (address) { |
||||
const { symbol = '', decimals = 0 } = await this.tokenInfoGetter(address) |
||||
|
||||
const autoFilled = Boolean(symbol && decimals) |
||||
this.setState({ autoFilled }) |
||||
this.handleCustomSymbolChange(symbol || '') |
||||
this.handleCustomDecimalsChange(decimals) |
||||
} |
||||
|
||||
handleCustomAddressChange (value) { |
||||
const customAddress = value.trim() |
||||
this.setState({ |
||||
customAddress, |
||||
customAddressError: null, |
||||
tokenSelectorError: null, |
||||
autoFilled: false, |
||||
}) |
||||
|
||||
const isValidAddress = ethUtil.isValidAddress(customAddress) |
||||
const standardAddress = ethUtil.addHexPrefix(customAddress).toLowerCase() |
||||
|
||||
switch (true) { |
||||
case !isValidAddress: |
||||
this.setState({ |
||||
customAddressError: this.context.t('invalidAddress'), |
||||
customSymbol: '', |
||||
customDecimals: 0, |
||||
customSymbolError: null, |
||||
customDecimalsError: null, |
||||
}) |
||||
|
||||
break |
||||
case Boolean(this.props.identities[standardAddress]): |
||||
this.setState({ |
||||
customAddressError: this.context.t('personalAddressDetected'), |
||||
}) |
||||
|
||||
break |
||||
case checkExistingAddresses(customAddress, this.props.tokens): |
||||
this.setState({ |
||||
customAddressError: this.context.t('tokenAlreadyAdded'), |
||||
}) |
||||
|
||||
break |
||||
default: |
||||
if (customAddress !== emptyAddr) { |
||||
this.attemptToAutoFillTokenParams(customAddress) |
||||
} |
||||
} |
||||
} |
||||
|
||||
handleCustomSymbolChange (value) { |
||||
const customSymbol = value.trim() |
||||
const symbolLength = customSymbol.length |
||||
let customSymbolError = null |
||||
|
||||
if (symbolLength <= 0 || symbolLength >= 10) { |
||||
customSymbolError = this.context.t('symbolBetweenZeroTen') |
||||
} |
||||
|
||||
this.setState({ customSymbol, customSymbolError }) |
||||
} |
||||
|
||||
handleCustomDecimalsChange (value) { |
||||
const customDecimals = value.trim() |
||||
const validDecimals = customDecimals !== null && |
||||
customDecimals !== '' && |
||||
customDecimals >= 0 && |
||||
customDecimals < 36 |
||||
let customDecimalsError = null |
||||
|
||||
if (!validDecimals) { |
||||
customDecimalsError = this.context.t('decimalsMustZerotoTen') |
||||
} |
||||
|
||||
this.setState({ customDecimals, customDecimalsError }) |
||||
} |
||||
|
||||
renderCustomTokenForm () { |
||||
const { |
||||
customAddress, |
||||
customSymbol, |
||||
customDecimals, |
||||
customAddressError, |
||||
customSymbolError, |
||||
customDecimalsError, |
||||
autoFilled, |
||||
} = this.state |
||||
|
||||
return ( |
||||
<div className="add-token__custom-token-form"> |
||||
<TextField |
||||
id="custom-address" |
||||
label="Token Address" |
||||
type="text" |
||||
value={customAddress} |
||||
onChange={e => this.handleCustomAddressChange(e.target.value)} |
||||
error={customAddressError} |
||||
fullWidth |
||||
margin="normal" |
||||
/> |
||||
<TextField |
||||
id="custom-symbol" |
||||
label="Token Symbol" |
||||
type="text" |
||||
value={customSymbol} |
||||
onChange={e => this.handleCustomSymbolChange(e.target.value)} |
||||
error={customSymbolError} |
||||
fullWidth |
||||
margin="normal" |
||||
disabled={autoFilled} |
||||
/> |
||||
<TextField |
||||
id="custom-decimals" |
||||
label="Decimals of Precision" |
||||
type="number" |
||||
value={customDecimals} |
||||
onChange={e => this.handleCustomDecimalsChange(e.target.value)} |
||||
error={customDecimalsError} |
||||
fullWidth |
||||
margin="normal" |
||||
disabled={autoFilled} |
||||
/> |
||||
</div> |
||||
) |
||||
} |
||||
|
||||
renderSearchToken () { |
||||
const { tokenSelectorError, selectedTokens, searchResults } = this.state |
||||
|
||||
return ( |
||||
<div className="add-token__search-token"> |
||||
<TokenSearch |
||||
onSearch={({ results = [] }) => this.setState({ searchResults: results })} |
||||
error={tokenSelectorError} |
||||
/> |
||||
<div className="add-token__token-list"> |
||||
<TokenList |
||||
results={searchResults} |
||||
selectedTokens={selectedTokens} |
||||
onToggleToken={token => this.handleToggleToken(token)} |
||||
/> |
||||
</div> |
||||
</div> |
||||
) |
||||
} |
||||
|
||||
render () { |
||||
const { displayedTab } = this.state |
||||
const { history, clearPendingTokens } = this.props |
||||
|
||||
return ( |
||||
<div className="page-container"> |
||||
<div className="page-container__header page-container__header--no-padding-bottom"> |
||||
<div className="page-container__title"> |
||||
{ this.context.t('addTokens') } |
||||
</div> |
||||
<div className="page-container__tabs"> |
||||
<div |
||||
className={classnames('page-container__tab', { |
||||
'page-container__tab--selected': displayedTab === SEARCH_TAB, |
||||
})} |
||||
onClick={() => this.setState({ displayedTab: SEARCH_TAB })} |
||||
> |
||||
{ this.context.t('search') } |
||||
</div> |
||||
<div |
||||
className={classnames('page-container__tab', { |
||||
'page-container__tab--selected': displayedTab === CUSTOM_TOKEN_TAB, |
||||
})} |
||||
onClick={() => this.setState({ displayedTab: CUSTOM_TOKEN_TAB })} |
||||
> |
||||
{ this.context.t('customToken') } |
||||
</div> |
||||
</div> |
||||
</div> |
||||
<div className="page-container__content"> |
||||
{ |
||||
displayedTab === CUSTOM_TOKEN_TAB |
||||
? this.renderCustomTokenForm() |
||||
: this.renderSearchToken() |
||||
} |
||||
</div> |
||||
<div className="page-container__footer"> |
||||
<Button |
||||
type="secondary" |
||||
large |
||||
className="page-container__footer-button" |
||||
onClick={() => { |
||||
clearPendingTokens() |
||||
history.push(DEFAULT_ROUTE) |
||||
}} |
||||
> |
||||
{ this.context.t('cancel') } |
||||
</Button> |
||||
<Button |
||||
type="primary" |
||||
large |
||||
className="page-container__footer-button" |
||||
onClick={() => this.handleNext()} |
||||
disabled={this.hasError() || !this.hasSelected()} |
||||
> |
||||
{ this.context.t('next') } |
||||
</Button> |
||||
</div> |
||||
</div> |
||||
) |
||||
} |
||||
} |
||||
|
||||
export default AddToken |
@ -0,0 +1,22 @@ |
||||
import { connect } from 'react-redux' |
||||
import AddToken from './add-token.component' |
||||
|
||||
const { setPendingTokens, clearPendingTokens } = require('../../../actions') |
||||
|
||||
const mapStateToProps = ({ metamask }) => { |
||||
const { identities, tokens, pendingTokens } = metamask |
||||
return { |
||||
identities, |
||||
tokens, |
||||
pendingTokens, |
||||
} |
||||
} |
||||
|
||||
const mapDispatchToProps = dispatch => { |
||||
return { |
||||
setPendingTokens: tokens => dispatch(setPendingTokens(tokens)), |
||||
clearPendingTokens: () => dispatch(clearPendingTokens()), |
||||
} |
||||
} |
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(AddToken) |
@ -0,0 +1,2 @@ |
||||
import AddToken from './add-token.container' |
||||
module.exports = AddToken |
@ -0,0 +1,25 @@ |
||||
@import './token-list/index'; |
||||
|
||||
.add-token { |
||||
&__custom-token-form { |
||||
padding: 8px 16px 16px; |
||||
|
||||
input[type="number"]::-webkit-inner-spin-button { |
||||
-webkit-appearance: none; |
||||
display: none; |
||||
} |
||||
|
||||
input[type="number"]:hover::-webkit-inner-spin-button { |
||||
-webkit-appearance: none; |
||||
display: none; |
||||
} |
||||
} |
||||
|
||||
&__search-token { |
||||
padding: 16px; |
||||
} |
||||
|
||||
&__token-list { |
||||
margin-top: 16px; |
||||
} |
||||
} |
@ -0,0 +1,2 @@ |
||||
import TokenList from './token-list.container' |
||||
module.exports = TokenList |
@ -0,0 +1,65 @@ |
||||
@import './token-list-placeholder/index'; |
||||
|
||||
.token-list { |
||||
&__title { |
||||
font-size: .75rem; |
||||
} |
||||
|
||||
&__tokens-container { |
||||
display: flex; |
||||
flex-direction: column; |
||||
} |
||||
|
||||
&__token { |
||||
transition: 200ms ease-in-out; |
||||
display: flex; |
||||
flex-flow: row nowrap; |
||||
align-items: center; |
||||
padding: 8px; |
||||
margin-top: 8px; |
||||
box-sizing: border-box; |
||||
border-radius: 10px; |
||||
cursor: pointer; |
||||
border: 2px solid transparent; |
||||
position: relative; |
||||
|
||||
&:hover { |
||||
border: 2px solid rgba($malibu-blue, .5); |
||||
} |
||||
|
||||
&--selected { |
||||
border: 2px solid $malibu-blue !important; |
||||
} |
||||
|
||||
&--disabled { |
||||
opacity: .4; |
||||
pointer-events: none; |
||||
} |
||||
} |
||||
|
||||
&__token-icon { |
||||
width: 48px; |
||||
height: 48px; |
||||
background-repeat: no-repeat; |
||||
background-size: contain; |
||||
background-position: center; |
||||
border-radius: 50%; |
||||
background-color: $white; |
||||
box-shadow: 0 2px 4px 0 rgba($black, .24); |
||||
margin-right: 12px; |
||||
flex: 0 0 auto; |
||||
} |
||||
|
||||
&__token-data { |
||||
display: flex; |
||||
flex-direction: row; |
||||
align-items: center; |
||||
min-width: 0; |
||||
} |
||||
|
||||
&__token-name { |
||||
overflow: hidden; |
||||
text-overflow: ellipsis; |
||||
white-space: nowrap; |
||||
} |
||||
} |
@ -0,0 +1,2 @@ |
||||
import TokenListPlaceholder from './token-list-placeholder.component' |
||||
module.exports = TokenListPlaceholder |
@ -0,0 +1,19 @@ |
||||
.token-list-placeholder { |
||||
display: flex; |
||||
align-items: center; |
||||
padding-top: 36px; |
||||
flex-direction: column; |
||||
line-height: 22px; |
||||
opacity: .5; |
||||
|
||||
&__text { |
||||
color: $silver-chalice; |
||||
width: 50%; |
||||
text-align: center; |
||||
margin-top: 8px; |
||||
} |
||||
|
||||
&__link { |
||||
color: $curious-blue; |
||||
} |
||||
} |
@ -0,0 +1,27 @@ |
||||
import React, { Component } from 'react' |
||||
import PropTypes from 'prop-types' |
||||
|
||||
export default class TokenListPlaceholder extends Component { |
||||
static contextTypes = { |
||||
t: PropTypes.func, |
||||
} |
||||
|
||||
render () { |
||||
return ( |
||||
<div className="token-list-placeholder"> |
||||
<img src="images/tokensearch.svg" /> |
||||
<div className="token-list-placeholder__text"> |
||||
{ this.context.t('addAcquiredTokens') } |
||||
</div> |
||||
<a |
||||
className="token-list-placeholder__link" |
||||
href="http://metamask.helpscoutdocs.com/article/16-managing-erc20-tokens" |
||||
target="_blank" |
||||
rel="noopener noreferrer" |
||||
> |
||||
{ this.context.t('learnMore') } |
||||
</a> |
||||
</div> |
||||
) |
||||
} |
||||
} |
@ -0,0 +1,60 @@ |
||||
import React, { Component } from 'react' |
||||
import PropTypes from 'prop-types' |
||||
import classnames from 'classnames' |
||||
import { checkExistingAddresses } from '../util' |
||||
import TokenListPlaceholder from './token-list-placeholder' |
||||
|
||||
export default class InfoBox extends Component { |
||||
static contextTypes = { |
||||
t: PropTypes.func, |
||||
} |
||||
|
||||
static propTypes = { |
||||
tokens: PropTypes.array, |
||||
results: PropTypes.array, |
||||
selectedTokens: PropTypes.object, |
||||
onToggleToken: PropTypes.func, |
||||
} |
||||
|
||||
render () { |
||||
const { results = [], selectedTokens = {}, onToggleToken, tokens = [] } = this.props |
||||
|
||||
return results.length === 0 |
||||
? <TokenListPlaceholder /> |
||||
: ( |
||||
<div className="token-list"> |
||||
<div className="token-list__title"> |
||||
{ this.context.t('searchResults') } |
||||
</div> |
||||
<div className="token-list__tokens-container"> |
||||
{ |
||||
Array(6).fill(undefined) |
||||
.map((_, i) => { |
||||
const { logo, symbol, name, address } = results[i] || {} |
||||
const tokenAlreadyAdded = checkExistingAddresses(address, tokens) |
||||
|
||||
return Boolean(logo || symbol || name) && ( |
||||
<div |
||||
className={classnames('token-list__token', { |
||||
'token-list__token--selected': selectedTokens[address], |
||||
'token-list__token--disabled': tokenAlreadyAdded, |
||||
})} |
||||
onClick={() => !tokenAlreadyAdded && onToggleToken(results[i])} |
||||
key={i} |
||||
> |
||||
<div |
||||
className="token-list__token-icon" |
||||
style={{ backgroundImage: logo && `url(images/contract/${logo})` }}> |
||||
</div> |
||||
<div className="token-list__token-data"> |
||||
<span className="token-list__token-name">{ `${name} (${symbol})` }</span> |
||||
</div> |
||||
</div> |
||||
) |
||||
}) |
||||
} |
||||
</div> |
||||
</div> |
||||
) |
||||
} |
||||
} |
@ -0,0 +1,11 @@ |
||||
import { connect } from 'react-redux' |
||||
import TokenList from './token-list.component' |
||||
|
||||
const mapStateToProps = ({ metamask }) => { |
||||
const { tokens } = metamask |
||||
return { |
||||
tokens, |
||||
} |
||||
} |
||||
|
||||
export default connect(mapStateToProps)(TokenList) |
@ -0,0 +1,2 @@ |
||||
import TokenSearch from './token-search.component' |
||||
module.exports = TokenSearch |
@ -0,0 +1,85 @@ |
||||
import React, { Component } from 'react' |
||||
import PropTypes from 'prop-types' |
||||
import contractMap from 'eth-contract-metadata' |
||||
import Fuse from 'fuse.js' |
||||
import InputAdornment from '@material-ui/core/InputAdornment' |
||||
import TextField from '../../../text-field' |
||||
|
||||
const contractList = Object.entries(contractMap) |
||||
.map(([ _, tokenData]) => tokenData) |
||||
.filter(tokenData => Boolean(tokenData.erc20)) |
||||
|
||||
const fuse = new Fuse(contractList, { |
||||
shouldSort: true, |
||||
threshold: 0.45, |
||||
location: 0, |
||||
distance: 100, |
||||
maxPatternLength: 32, |
||||
minMatchCharLength: 1, |
||||
keys: [ |
||||
{ name: 'name', weight: 0.5 }, |
||||
{ name: 'symbol', weight: 0.5 }, |
||||
], |
||||
}) |
||||
|
||||
export default class TokenSearch extends Component { |
||||
static contextTypes = { |
||||
t: PropTypes.func, |
||||
} |
||||
|
||||
static defaultProps = { |
||||
error: null, |
||||
} |
||||
|
||||
static propTypes = { |
||||
onSearch: PropTypes.func, |
||||
error: PropTypes.string, |
||||
} |
||||
|
||||
constructor (props) { |
||||
super(props) |
||||
|
||||
this.state = { |
||||
searchQuery: '', |
||||
} |
||||
} |
||||
|
||||
handleSearch (searchQuery) { |
||||
this.setState({ searchQuery }) |
||||
const fuseSearchResult = fuse.search(searchQuery) |
||||
const addressSearchResult = contractList.filter(token => { |
||||
return token.address.toLowerCase() === searchQuery.toLowerCase() |
||||
}) |
||||
const results = [...addressSearchResult, ...fuseSearchResult] |
||||
this.props.onSearch({ searchQuery, results }) |
||||
} |
||||
|
||||
renderAdornment () { |
||||
return ( |
||||
<InputAdornment |
||||
position="start" |
||||
style={{ marginRight: '12px' }} |
||||
> |
||||
<img src="images/search.svg" /> |
||||
</InputAdornment> |
||||
) |
||||
} |
||||
|
||||
render () { |
||||
const { error } = this.props |
||||
const { searchQuery } = this.state |
||||
|
||||
return ( |
||||
<TextField |
||||
id="search-tokens" |
||||
placeholder={this.context.t('searchTokens')} |
||||
type="text" |
||||
value={searchQuery} |
||||
onChange={e => this.handleSearch(e.target.value)} |
||||
error={error} |
||||
fullWidth |
||||
startAdornment={this.renderAdornment()} |
||||
/> |
||||
) |
||||
} |
||||
} |
@ -0,0 +1,13 @@ |
||||
import R from 'ramda' |
||||
|
||||
export function checkExistingAddresses (address, tokenList = []) { |
||||
if (!address) { |
||||
return false |
||||
} |
||||
|
||||
const matchesAddress = existingToken => { |
||||
return existingToken.address.toLowerCase() === address.toLowerCase() |
||||
} |
||||
|
||||
return R.any(matchesAddress)(tokenList) |
||||
} |
@ -0,0 +1,115 @@ |
||||
import React, { Component } from 'react' |
||||
import PropTypes from 'prop-types' |
||||
import { DEFAULT_ROUTE, ADD_TOKEN_ROUTE } from '../../../routes' |
||||
import Button from '../../button' |
||||
import Identicon from '../../../components/identicon' |
||||
import TokenBalance from './token-balance' |
||||
|
||||
export default class ConfirmAddToken extends Component { |
||||
static contextTypes = { |
||||
t: PropTypes.func, |
||||
} |
||||
|
||||
static propTypes = { |
||||
history: PropTypes.object, |
||||
clearPendingTokens: PropTypes.func, |
||||
addTokens: PropTypes.func, |
||||
pendingTokens: PropTypes.object, |
||||
} |
||||
|
||||
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 { history, addTokens, clearPendingTokens, pendingTokens } = this.props |
||||
|
||||
return ( |
||||
<div className="page-container"> |
||||
<div className="page-container__header"> |
||||
<div className="page-container__title"> |
||||
{ this.context.t('addTokens') } |
||||
</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 } = 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} |
||||
/> |
||||
<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="secondary" |
||||
large |
||||
className="page-container__footer-button" |
||||
onClick={() => history.push(ADD_TOKEN_ROUTE)} |
||||
> |
||||
{ this.context.t('back') } |
||||
</Button> |
||||
<Button |
||||
type="primary" |
||||
large |
||||
className="page-container__footer-button" |
||||
onClick={() => { |
||||
addTokens(pendingTokens) |
||||
.then(() => { |
||||
clearPendingTokens() |
||||
history.push(DEFAULT_ROUTE) |
||||
}) |
||||
}} |
||||
> |
||||
{ this.context.t('addTokens') } |
||||
</Button> |
||||
</div> |
||||
</div> |
||||
) |
||||
} |
||||
} |
@ -0,0 +1,20 @@ |
||||
import { connect } from 'react-redux' |
||||
import ConfirmAddToken from './confirm-add-token.component' |
||||
|
||||
const { addTokens, clearPendingTokens } = require('../../../actions') |
||||
|
||||
const mapStateToProps = ({ metamask }) => { |
||||
const { pendingTokens } = metamask |
||||
return { |
||||
pendingTokens, |
||||
} |
||||
} |
||||
|
||||
const mapDispatchToProps = dispatch => { |
||||
return { |
||||
addTokens: tokens => dispatch(addTokens(tokens)), |
||||
clearPendingTokens: () => dispatch(clearPendingTokens()), |
||||
} |
||||
} |
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(ConfirmAddToken) |
@ -0,0 +1,2 @@ |
||||
import ConfirmAddToken from './confirm-add-token.container' |
||||
module.exports = ConfirmAddToken |
@ -0,0 +1,69 @@ |
||||
.confirm-add-token { |
||||
padding: 16px; |
||||
|
||||
&__header { |
||||
font-size: .75rem; |
||||
display: flex; |
||||
} |
||||
|
||||
&__token { |
||||
flex: 1; |
||||
min-width: 0; |
||||
} |
||||
|
||||
&__balance { |
||||
flex: 0 0 30%; |
||||
min-width: 0; |
||||
} |
||||
|
||||
&__token-list { |
||||
display: flex; |
||||
flex-flow: column nowrap; |
||||
|
||||
.token-balance { |
||||
display: flex; |
||||
flex-flow: row nowrap; |
||||
align-items: flex-start; |
||||
|
||||
&__amount { |
||||
color: $scorpion; |
||||
font-size: 43px; |
||||
line-height: 43px; |
||||
margin-right: 8px; |
||||
} |
||||
|
||||
&__symbol { |
||||
color: $scorpion; |
||||
font-size: 16px; |
||||
font-weight: 400; |
||||
line-height: 24px; |
||||
} |
||||
} |
||||
} |
||||
|
||||
&__token-list-item { |
||||
display: flex; |
||||
flex-flow: row nowrap; |
||||
align-items: center; |
||||
margin-top: 8px; |
||||
box-sizing: border-box; |
||||
} |
||||
|
||||
&__data { |
||||
display: flex; |
||||
align-items: center; |
||||
padding: 8px; |
||||
} |
||||
|
||||
&__name { |
||||
min-width: 0; |
||||
white-space: nowrap; |
||||
overflow: hidden; |
||||
text-overflow: ellipsis; |
||||
} |
||||
|
||||
&__token-icon { |
||||
margin-right: 12px; |
||||
flex: 0 0 auto; |
||||
} |
||||
} |
@ -0,0 +1,2 @@ |
||||
import TokenBalance from './token-balance.container' |
||||
module.exports = TokenBalance |
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in new issue