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 assert = require('assert') |
||||||
const ComposableObservableStore = require('../../app/scripts/lib/ComposableObservableStore') |
const ComposableObservableStore = require('../../../app/scripts/lib/ComposableObservableStore') |
||||||
const ObservableStore = require('obs-store') |
const ObservableStore = require('obs-store') |
||||||
|
|
||||||
describe('ComposableObservableStore', () => { |
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,26 +1,26 @@ |
|||||||
const assert = require('assert') |
const assert = require('assert') |
||||||
const AddressBookController = require('../../app/scripts/controllers/address-book') |
const AddressBookController = require('../../../../app/scripts/controllers/address-book') |
||||||
|
|
||||||
const mockKeyringController = { |
const stubPreferencesStore = { |
||||||
memStore: { |
getState: function () { |
||||||
getState: function () { |
return { |
||||||
return { |
identities: { |
||||||
identities: { |
'0x0aaa': { |
||||||
'0x0aaa': { |
address: '0x0aaa', |
||||||
address: '0x0aaa', |
name: 'owned', |
||||||
name: 'owned', |
|
||||||
}, |
|
||||||
}, |
}, |
||||||
} |
}, |
||||||
}, |
} |
||||||
}, |
}, |
||||||
} |
}; |
||||||
|
|
||||||
describe('address-book-controller', function () { |
describe('address-book-controller', function () { |
||||||
var addressBookController |
var addressBookController |
||||||
|
|
||||||
beforeEach(function () { |
beforeEach(function () { |
||||||
addressBookController = new AddressBookController({}, mockKeyringController) |
addressBookController = new AddressBookController({ |
||||||
|
preferencesStore: stubPreferencesStore, |
||||||
|
}) |
||||||
}) |
}) |
||||||
|
|
||||||
describe('addres book management', function () { |
describe('addres book management', function () { |
@ -1,5 +1,5 @@ |
|||||||
const assert = require('assert') |
const assert = require('assert') |
||||||
const BlacklistController = require('../../app/scripts/controllers/blacklist') |
const BlacklistController = require('../../../../app/scripts/controllers/blacklist') |
||||||
|
|
||||||
describe('blacklist controller', function () { |
describe('blacklist controller', function () { |
||||||
let blacklistController |
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 assert = require('assert') |
||||||
const nock = require('nock') |
const nock = require('nock') |
||||||
const NetworkController = require('../../app/scripts/controllers/network') |
const NetworkController = require('../../../../app/scripts/controllers/network') |
||||||
const { |
const { |
||||||
getNetworkDisplayName, |
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 = {} |
const providerResultStub = {} |
||||||
|
|
||||||
describe('# Network Controller', function () { |
describe('# Network Controller', function () { |
@ -1,6 +1,6 @@ |
|||||||
const assert = require('assert') |
const assert = require('assert') |
||||||
const configManagerGen = require('../lib/mock-config-manager') |
const configManagerGen = require('../../../lib/mock-config-manager') |
||||||
const NoticeController = require('../../app/scripts/notice-controller') |
const NoticeController = require('../../../../app/scripts/notice-controller') |
||||||
|
|
||||||
describe('notice-controller', function () { |
describe('notice-controller', function () { |
||||||
var noticeController |
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 assert = require('assert') |
||||||
const sinon = require('sinon') |
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') |
const ObservableStore = require('obs-store') |
||||||
|
|
||||||
describe('TokenRatesController', () => { |
describe('TokenRatesController', () => { |
@ -1,6 +1,6 @@ |
|||||||
const assert = require('assert') |
const assert = require('assert') |
||||||
const NonceTracker = require('../../app/scripts/controllers/transactions/nonce-tracker') |
const NonceTracker = require('../../../../../app/scripts/controllers/transactions/nonce-tracker') |
||||||
const MockTxGen = require('../lib/mock-tx-gen') |
const MockTxGen = require('../../../../lib/mock-tx-gen') |
||||||
let providerResultStub = {} |
let providerResultStub = {} |
||||||
|
|
||||||
describe('Nonce Tracker', function () { |
describe('Nonce Tracker', function () { |
@ -1,5 +1,5 @@ |
|||||||
const assert = require('assert') |
const assert = require('assert') |
||||||
const txHelper = require('../../ui/lib/tx-helper') |
const txHelper = require('../../../../../ui/lib/tx-helper') |
||||||
|
|
||||||
describe('txHelper', function () { |
describe('txHelper', function () { |
||||||
it('always shows the oldest tx first', function () { |
it('always shows the oldest tx first', function () { |
@ -1,6 +1,6 @@ |
|||||||
const assert = require('assert') |
const assert = require('assert') |
||||||
const txStateHistoryHelper = require('../../app/scripts/controllers/transactions/lib/tx-state-history-helper') |
const txStateHistoryHelper = require('../../../../../app/scripts/controllers/transactions/lib/tx-state-history-helper') |
||||||
const testVault = require('../data/v17-long-history.json') |
const testVault = require('../../../../data/v17-long-history.json') |
||||||
|
|
||||||
describe ('Transaction state history helper', function () { |
describe ('Transaction state history helper', function () { |
||||||
|
|
@ -1,8 +1,8 @@ |
|||||||
const assert = require('assert') |
const assert = require('assert') |
||||||
const clone = require('clone') |
const clone = require('clone') |
||||||
const ObservableStore = require('obs-store') |
const ObservableStore = require('obs-store') |
||||||
const TxStateManager = require('../../app/scripts/controllers/transactions/tx-state-manager') |
const TxStateManager = require('../../../../../app/scripts/controllers/transactions/tx-state-manager') |
||||||
const txStateHistoryHelper = require('../../app/scripts/controllers/transactions/lib/tx-state-history-helper') |
const txStateHistoryHelper = require('../../../../../app/scripts/controllers/transactions/lib/tx-state-history-helper') |
||||||
const noop = () => true |
const noop = () => true |
||||||
|
|
||||||
describe('TransactionStateManager', function () { |
describe('TransactionStateManager', function () { |
@ -1,5 +1,5 @@ |
|||||||
const assert = require('assert') |
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 () { |
describe('txUtils', function () { |
@ -1,6 +1,6 @@ |
|||||||
const assert = require('assert') |
const assert = require('assert') |
||||||
|
|
||||||
const EdgeEncryptor = require('../../app/scripts/edge-encryptor') |
const EdgeEncryptor = require('../../../app/scripts/edge-encryptor') |
||||||
|
|
||||||
var password = 'passw0rd1' |
var password = 'passw0rd1' |
||||||
var data = 'some random data' |
var data = 'some random data' |
@ -1,5 +1,5 @@ |
|||||||
const assert = require('assert') |
const assert = require('assert') |
||||||
const MessageManager = require('../../app/scripts/lib/message-manager') |
const MessageManager = require('../../../app/scripts/lib/message-manager') |
||||||
|
|
||||||
describe('Message Manager', function () { |
describe('Message Manager', function () { |
||||||
let messageManager |
let messageManager |
@ -1,5 +1,5 @@ |
|||||||
const assert = require('assert') |
const assert = require('assert') |
||||||
const nodeify = require('../../app/scripts/lib/nodeify') |
const nodeify = require('../../../app/scripts/lib/nodeify') |
||||||
|
|
||||||
describe('nodeify', function () { |
describe('nodeify', function () { |
||||||
var obj = { |
var obj = { |
@ -1,6 +1,6 @@ |
|||||||
const assert = require('assert') |
const assert = require('assert') |
||||||
const PendingBalanceCalculator = require('../../app/scripts/lib/pending-balance-calculator') |
const PendingBalanceCalculator = require('../../../app/scripts/lib/pending-balance-calculator') |
||||||
const MockTxGen = require('../lib/mock-tx-gen') |
const MockTxGen = require('../../lib/mock-tx-gen') |
||||||
const BN = require('ethereumjs-util').BN |
const BN = require('ethereumjs-util').BN |
||||||
let providerResultStub = {} |
let providerResultStub = {} |
||||||
|
|
@ -1,6 +1,6 @@ |
|||||||
const assert = require('assert') |
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 () { |
describe('Personal Message Manager', function () { |
||||||
let messageManager |
let messageManager |
@ -1,9 +1,9 @@ |
|||||||
const assert = require('assert') |
const assert = require('assert') |
||||||
const clone = require('clone') |
const clone = require('clone') |
||||||
const KeyringController = require('eth-keyring-controller') |
const KeyringController = require('eth-keyring-controller') |
||||||
const firstTimeState = require('../../app/scripts/first-time-state') |
const firstTimeState = require('../../../app/scripts/first-time-state') |
||||||
const seedPhraseVerifier = require('../../app/scripts/lib/seed-phrase-verifier') |
const seedPhraseVerifier = require('../../../app/scripts/lib/seed-phrase-verifier') |
||||||
const mockEncryptor = require('../lib/mock-encryptor') |
const mockEncryptor = require('../../lib/mock-encryptor') |
||||||
|
|
||||||
describe('SeedPhraseVerifier', function () { |
describe('SeedPhraseVerifier', function () { |
||||||
|
|
@ -1,5 +1,5 @@ |
|||||||
const assert = require('assert') |
const assert = require('assert') |
||||||
const { sufficientBalance } = require('../../app/scripts/lib/util') |
const { sufficientBalance } = require('../../../app/scripts/lib/util') |
||||||
|
|
||||||
|
|
||||||
describe('SufficientBalance', function () { |
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 assert = require('assert') |
||||||
const path = require('path') |
const path = require('path') |
||||||
|
|
||||||
const wallet1 = require(path.join('..', 'lib', 'migrations', '001.json')) |
const wallet1 = require(path.join('..', '..', 'lib', 'migrations', '001.json')) |
||||||
const vault4 = require(path.join('..', 'lib', 'migrations', '004.json')) |
const vault4 = require(path.join('..', '..', 'lib', 'migrations', '004.json')) |
||||||
let vault5, vault6, vault7, vault8, vault9 // vault10, vault11
|
let vault5, vault6, vault7, vault8, vault9 // vault10, vault11
|
||||||
|
|
||||||
const migration2 = require(path.join('..', '..', 'app', 'scripts', 'migrations', '002')) |
const migration2 = require(path.join('..', '..', '..', 'app', 'scripts', 'migrations', '002')) |
||||||
const migration3 = require(path.join('..', '..', 'app', 'scripts', 'migrations', '003')) |
const migration3 = require(path.join('..', '..', '..', 'app', 'scripts', 'migrations', '003')) |
||||||
const migration4 = require(path.join('..', '..', 'app', 'scripts', 'migrations', '004')) |
const migration4 = require(path.join('..', '..', '..', 'app', 'scripts', 'migrations', '004')) |
||||||
const migration5 = require(path.join('..', '..', 'app', 'scripts', 'migrations', '005')) |
const migration5 = require(path.join('..', '..', '..', 'app', 'scripts', 'migrations', '005')) |
||||||
const migration6 = require(path.join('..', '..', 'app', 'scripts', 'migrations', '006')) |
const migration6 = require(path.join('..', '..', '..', 'app', 'scripts', 'migrations', '006')) |
||||||
const migration7 = require(path.join('..', '..', 'app', 'scripts', 'migrations', '007')) |
const migration7 = require(path.join('..', '..', '..', 'app', 'scripts', 'migrations', '007')) |
||||||
const migration8 = require(path.join('..', '..', 'app', 'scripts', 'migrations', '008')) |
const migration8 = require(path.join('..', '..', '..', 'app', 'scripts', 'migrations', '008')) |
||||||
const migration9 = require(path.join('..', '..', 'app', 'scripts', 'migrations', '009')) |
const migration9 = require(path.join('..', '..', '..', 'app', 'scripts', 'migrations', '009')) |
||||||
const migration10 = require(path.join('..', '..', 'app', 'scripts', 'migrations', '010')) |
const migration10 = require(path.join('..', '..', '..', 'app', 'scripts', 'migrations', '010')) |
||||||
const migration11 = require(path.join('..', '..', 'app', 'scripts', 'migrations', '011')) |
const migration11 = require(path.join('..', '..', '..', 'app', 'scripts', 'migrations', '011')) |
||||||
const migration12 = require(path.join('..', '..', 'app', 'scripts', 'migrations', '012')) |
const migration12 = require(path.join('..', '..', '..', 'app', 'scripts', 'migrations', '012')) |
||||||
const migration13 = require(path.join('..', '..', 'app', 'scripts', 'migrations', '013')) |
const migration13 = require(path.join('..', '..', '..', 'app', 'scripts', 'migrations', '013')) |
||||||
|
|
||||||
|
|
||||||
const oldTestRpc = 'https://rawtestrpc.metamask.io/' |
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 |
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