Merge pull request #452 from poanetwork/develop

5.2.4
This commit is contained in:
Victor Baranov 2021-02-27 22:40:11 +03:00 committed by GitHub
commit e283648d60
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
133 changed files with 26025 additions and 6472 deletions

View File

@ -31,14 +31,6 @@ workflows:
- test-unit:
requires:
- prep-deps-npm
# - test-integration-mascara-chrome:
# requires:
# - prep-deps-npm
# - prep-scss
# - test-integration-mascara-firefox:
# requires:
# - prep-deps-npm
# - prep-scss
- test-integration-flat-chrome:
requires:
- prep-deps-npm
@ -53,8 +45,6 @@ workflows:
- test-unit
- test-e2e-chrome
# - test-e2e-firefox
# - test-integration-mascara-chrome
# - test-integration-mascara-firefox
- test-integration-flat-chrome
- test-integration-flat-firefox
- job-screens:
@ -292,9 +282,6 @@ jobs:
key: build-cache-{{ .Revision }}
- restore_cache:
key: job-screens-{{ .Revision }}
- store_artifacts:
path: dist/mascara
destination: builds/mascara
- store_artifacts:
path: dist/sourcemaps
destination: builds/sourcemaps
@ -380,37 +367,6 @@ jobs:
name: test:integration:flat
command: npm run test:flat
test-integration-mascara-firefox:
environment:
browsers: '["Firefox"]'
docker:
- image: circleci/node:10.19.0-browsers
steps:
- checkout
- restore_cache:
key: dependency-cache-firefox-{{ .Revision }}
- run:
name: Install firefox
command: ./.circleci/scripts/firefox-install
- restore_cache:
key: dependency-cache-{{ .Revision }}
- run:
name: test:integration:mascara
command: npm run test:mascara
test-integration-mascara-chrome:
environment:
browsers: '["Chrome"]'
docker:
- image: circleci/node:10.19.0-browsers
steps:
- checkout
- restore_cache:
key: dependency-cache-{{ .Revision }}
- run:
name: test:integration:mascara
command: npm run test:mascara
all-tests-pass:
docker:
- image: circleci/node:10.19.0-browsers

View File

@ -15,9 +15,6 @@ app/vendor/**
ui/lib/blockies.js
mascara/src/app/first-time/spinner.js
mascara/test/jquery-3.1.0.min.js
test/integration/bundle.js
test/integration/jquery-3.1.0.min.js
test/integration/helpers.js

View File

@ -1,2 +1,3 @@
; Extra environment variables
ETH_MAINNET_RPC_ENDPOINT=00000000000
INFURA_PROJECT_ID=00000000000

View File

@ -4,7 +4,6 @@ dist/
docs/
fonts/
images/
mascara/
node_modules/
notices/
test/

View File

@ -2,6 +2,12 @@
## Current Master
## 5.2.4 Sat Feb 27 2021
- [#451](https://github.com/poanetwork/nifty-wallet/pull/451) - (Fix) Fix export private key when switching between chains
- [#450](https://github.com/poanetwork/nifty-wallet/pull/450) - (Chore) Remove deprecated Infura network status check
- [#443](https://github.com/poanetwork/nifty-wallet/pull/443) - (Fix) Fire 'confirmation', 'receipt' events
## 5.2.3 Fri Jan 15 2021
- [#441](https://github.com/poanetwork/nifty-wallet/pull/441) - Replace Infura Mainnet endpoint with custom one

View File

@ -1,7 +1,7 @@
{
"name": "__MSG_appName__",
"short_name": "__MSG_appName__",
"version": "5.2.3",
"version": "5.2.4",
"manifest_version": 2,
"author": "POA Network",
"description": "__MSG_appDescription__",

View File

@ -10,22 +10,19 @@ import pump from 'pump'
import debounce from 'debounce-stream'
import log from 'loglevel'
import extension from 'extensionizer'
const LocalStorageStore = require('obs-store/lib/localStorage')
const LocalStore = require('./lib/local-store')
const storeTransform = require('obs-store/lib/transform')
const asStream = require('obs-store/lib/asStream')
const ExtensionPlatform = require('./platforms/extension')
const Migrator = require('./lib/migrator/')
const migrations = require('./migrations/')
const PortStream = require('extension-port-stream')
const createStreamSink = require('./lib/createStreamSink')
import LocalStore from './lib/local-store'
import { storeAsStream, storeTransformStream } from '@metamask/obs-store'
import ExtensionPlatform from './platforms/extension'
import migrations from './migrations'
import Migrator from './lib/migrator'
import PortStream from 'extension-port-stream'
import createStreamSink from './lib/createStreamSink'
import NotificationManager from './lib/notification-manager.js'
const MetamaskController = require('./metamask-controller')
const rawFirstTimeState = require('./first-time-state')
import rawFirstTimeState from './first-time-state'
const setupRaven = require('./lib/setupRaven')
const reportFailedTxToSentry = require('./lib/reportFailedTxToSentry')
const getFirstPreferredLangCode = require('./lib/get-first-preferred-lang-code')
const getObjStructure = require('./lib/getObjStructure')
import getFirstPreferredLangCode from './lib/get-first-preferred-lang-code'
import getObjStructure from './lib/getObjStructure'
const {
ENVIRONMENT_TYPE_POPUP,
@ -36,12 +33,12 @@ const {
// METAMASK_TEST_CONFIG is used in e2e tests to set the default network to localhost
const firstTimeState = Object.assign({}, rawFirstTimeState, global.METAMASK_TEST_CONFIG)
const STORAGE_KEY = 'metamask-config'
const METAMASK_DEBUG = process.env.METAMASK_DEBUG
log.setDefaultLevel(METAMASK_DEBUG ? 'debug' : 'warn')
const platform = new ExtensionPlatform()
const notificationManager = new NotificationManager()
global.METAMASK_NOTIFIER = notificationManager
@ -55,7 +52,6 @@ const openMetamaskTabsIDs = {}
const requestAccountTabIds = {}
// state persistence
const diskStore = new LocalStorageStore({ storageKey: STORAGE_KEY })
const localStore = new LocalStore()
let versionedData
@ -74,9 +70,6 @@ initialize().catch(log.error)
* @property {boolean} loadingDefaults - TODO: Document
* @property {Object} txParams - The tx params as passed to the network provider.
* @property {Object[]} history - A history of mutations to this TransactionMeta object.
* @property {boolean} gasPriceSpecified - True if the suggesting dapp specified a gas price, prevents auto-estimation.
* @property {boolean} gasLimitSpecified - True if the suggesting dapp specified a gas limit, prevents auto-estimation.
* @property {string} estimatedGas - A hex string represented the estimated gas limit required to complete the transaction.
* @property {string} origin - A string representing the interface that suggested the transaction.
* @property {Object} nonceDetails - A metadata object containing information used to derive the suggested nonce, useful for debugging nonce issues.
* @property {string} rawTx - A hex string of the final signed transaction, ready to submit to the network.
@ -90,23 +83,15 @@ initialize().catch(log.error)
* @property {boolean} isInitialized - Whether the first vault has been created.
* @property {boolean} isUnlocked - Whether the vault is currently decrypted and accounts are available for selection.
* @property {boolean} isAccountMenuOpen - Represents whether the main account selection UI is currently displayed.
* @property {boolean} isMascara - True if the current context is the extensionless MetaMascara project.
* @property {boolean} isPopup - Returns true if the current view is an externally-triggered notification.
* @property {string} rpcTarget - DEPRECATED - The URL of the current RPC provider.
* @property {Object} identities - An object matching lower-case hex addresses to Identity objects with "address" and "name" (nickname) keys.
* @property {Object} unapprovedTxs - An object mapping transaction hashes to unapproved transactions.
* @property {boolean} noActiveNotices - False if there are notices the user should confirm before using the application.
* @property {Array} frequentRpcList - A list of frequently used RPCs, including custom user-provided ones.
* @property {Array} addressBook - A list of previously sent to addresses.
* @property {address} selectedTokenAddress - Used to indicate if a token is globally selected. Should be deprecated in favor of UI-centric token selection.
* @property {Object} tokenExchangeRates - Info about current token prices.
* @property {Object} contractExchangeRates - Info about current token prices.
* @property {Array} tokens - Tokens held by the current user, including their balances.
* @property {Object} send - TODO: Document
* @property {Object} coinOptions - TODO: Document
* @property {boolean} useBlockie - Indicates preferred user identicon format. True for blockie, false for Jazzicon.
* @property {Object} featureFlags - An object for optional feature flags.
* @property {string} networkEndpointType - TODO: Document
* @property {boolean} isRevealingSeedWords - True if seed words are currently being recovered, and should be shown to user.
* @property {boolean} welcomeScreen - True if welcome screen should be shown.
* @property {string} currentLocale - A locale string matching the user's preferred display language.
* @property {Object} provider - The current selected network provider.
@ -116,24 +101,24 @@ initialize().catch(log.error)
* @property {string} dPath - A path to derive accounts.
* @property {Object} accounts - An object mapping lower-case hex addresses to objects with "balance" and "address" keys, both storing hex string values.
* @property {hex} currentBlockGasLimit - The most recently seen block gas limit, in a lower case hex prefixed string.
* @property {TransactionMeta[]} selectedAddressTxList - An array of transactions associated with the currently selected account.
* @property {Object} unapprovedMsgs - An object of messages associated with the currently selected account, mapping a unique ID to the options.
* @property {TransactionMeta[]} currentNetworkTxList - An array of transactions associated with the currently selected network.
* @property {Object} unapprovedMsgs - An object of messages pending approval, mapping a unique ID to the options.
* @property {number} unapprovedMsgCount - The number of messages in unapprovedMsgs.
* @property {Object} unapprovedPersonalMsgs - An object of messages associated with the currently selected account, mapping a unique ID to the options.
* @property {Object} unapprovedPersonalMsgs - An object of messages pending approval, mapping a unique ID to the options.
* @property {number} unapprovedPersonalMsgCount - The number of messages in unapprovedPersonalMsgs.
* @property {Object} unapprovedTypedMsgs - An object of messages associated with the currently selected account, mapping a unique ID to the options.
* @property {Object} unapprovedEncryptionPublicKeyMsgs - An object of messages pending approval, mapping a unique ID to the options.
* @property {number} unapprovedEncryptionPublicKeyMsgCount - The number of messages in EncryptionPublicKeyMsgs.
* @property {Object} unapprovedDecryptMsgs - An object of messages pending approval, mapping a unique ID to the options.
* @property {number} unapprovedDecryptMsgCount - The number of messages in unapprovedDecryptMsgs.
* @property {Object} unapprovedTypedMsgs - An object of messages pending approval, mapping a unique ID to the options.
* @property {number} unapprovedTypedMsgCount - The number of messages in unapprovedTypedMsgs.
* @property {number} pendingApprovalCount - The number of pending request in the approval controller.
* @property {string[]} keyringTypes - An array of unique keyring identifying strings, representing available strategies for creating accounts.
* @property {Keyring[]} keyrings - An array of keyring descriptions, summarizing the accounts that are available for use, and what keyrings they belong to.
* @property {Object} computedBalances - Maps accounts to their balances, accounting for balance changes from pending transactions.
* @property {string} currentAccountTab - A view identifying string for displaying the current displayed view, allows user to have a preferred tab in the old UI (between tokens and history).
* @property {string} selectedAddress - A lower case hex string of the currently selected address.
* @property {string} currentCurrency - A string identifying the user's preferred display currency, for use in showing conversion rates.
* @property {number} conversionRate - A number representing the current exchange rate from the user's preferred currency to Ether.
* @property {number} conversionDate - A unix epoch date (ms) for the time the current conversion rate was last retrieved.
* @property {Object} infuraNetworkStatus - An object of infura network status checks.
* @property {Block[]} recentBlocks - An array of recent blocks, used to calculate an effective but cheap gas price.
* @property {Array} shapeShiftTxList - An array of objects describing shapeshift exchange attempts.
* @property {boolean} forgottenPassword - Returns true if the user has initiated the password recovery screen, is recovering from seed phrase.
*/
@ -170,30 +155,17 @@ async function loadStateFromPersistence () {
// read from disk
// first from preferred, async API:
versionedData = (await localStore.get()) ||
diskStore.getState() ||
migrator.generateInitialState(firstTimeState)
versionedData =
(await localStore.get()) || migrator.generateInitialState(firstTimeState)
// check if somehow state is empty
// this should never happen but new error reporting suggests that it has
// for a small number of users
// https://github.com/metamask/metamask-extension/issues/3919
if (versionedData && !versionedData.data) {
// try to recover from diskStore incase only localStore is bad
const diskStoreState = diskStore.getState()
if (diskStoreState && diskStoreState.data) {
// we were able to recover (though it might be old)
versionedData = diskStoreState
const vaultStructure = getObjStructure(versionedData)
raven.captureMessage('Nifty Wallet - Empty vault found - recovered from diskStore', {
// "extra" key is required by Sentry
extra: { vaultStructure },
})
} else {
// unable to recover, clear state
versionedData = migrator.generateInitialState(firstTimeState)
raven.captureMessage('Nifty Wallet - Empty vault found - unable to recover')
}
// unable to recover, clear state
versionedData = migrator.generateInitialState(firstTimeState)
raven.captureMessage('Nifty Wallet - Empty vault found - unable to recover')
}
// report migration errors to sentry
@ -242,6 +214,7 @@ function setupController (initState, initLangCode) {
//
const controller = new MetamaskController({
infuraProjectId: process.env.INFURA_PROJECT_ID,
ethMainnetRpcEndpoint: process.env.ETH_MAINNET_RPC_ENDPOINT,
// User confirmation callbacks:
showUnconfirmedMessage: triggerUi,
@ -254,6 +227,7 @@ function setupController (initState, initLangCode) {
initLangCode,
// platform specific api
platform,
extension,
getRequestAccountTabIds: () => {
return requestAccountTabIds
},
@ -263,22 +237,11 @@ function setupController (initState, initLangCode) {
})
global.metamaskController = controller
// report failed transactions to Sentry
controller.txController.on(`tx:status-update`, (txId, status) => {
if (status !== 'failed') return
const txMeta = controller.txController.txStateManager.getTx(txId)
try {
reportFailedTxToSentry({ raven, txMeta })
} catch (e) {
console.error(e)
}
})
// setup state persistence
pump(
asStream(controller.store),
storeAsStream(controller.store),
debounce(1000),
storeTransform(versionifyData),
storeTransformStream(versionifyData),
createStreamSink(persistData),
(error) => {
log.error('Nifty Wallet - Persistence pipeline failed', error)
@ -295,6 +258,8 @@ function setupController (initState, initLangCode) {
return versionedData
}
let dataPersistenceFailing = false
async function persistData (state) {
if (!state) {
throw new Error('Nifty Wallet - updated state is missing')
@ -305,8 +270,14 @@ function setupController (initState, initLangCode) {
if (localStore.isSupported) {
try {
await localStore.set(state)
if (dataPersistenceFailing) {
dataPersistenceFailing = false
}
} catch (err) {
// log error so we dont break the pipeline
if (!dataPersistenceFailing) {
dataPersistenceFailing = true
}
log.error('error setting state in local store:', err)
}
}
@ -324,7 +295,7 @@ function setupController (initState, initLangCode) {
[ENVIRONMENT_TYPE_FULLSCREEN]: true,
}
const metamaskBlacklistedPorts = [
const metamaskBlockedPorts = [
'trezor-connect',
]
@ -348,8 +319,8 @@ function setupController (initState, initLangCode) {
const processName = remotePort.name
const isMetaMaskInternalProcess = metamaskInternalProcessHash[processName]
if (metamaskBlacklistedPorts.includes(remotePort.name)) {
return false
if (metamaskBlockedPorts.includes(remotePort.name)) {
return
}
if (isMetaMaskInternalProcess) {
@ -389,7 +360,7 @@ function setupController (initState, initLangCode) {
if (remotePort.sender && remotePort.sender.tab && remotePort.sender.url) {
const tabId = remotePort.sender.tab.id
const url = new URL(remotePort.sender.url)
const origin = url.hostname
const { origin } = url
remotePort.onMessage.addListener((msg) => {
if (msg.data && msg.data.method === 'eth_requestAccounts') {
@ -426,11 +397,13 @@ function setupController (initState, initLangCode) {
function updateBadge () {
let label = ''
const unapprovedTxCount = controller.txController.getUnapprovedTxCount()
const unapprovedMsgCount = controller.messageManager.unapprovedMsgCount
const unapprovedPersonalMsgCount = controller.personalMessageManager.unapprovedPersonalMsgCount
const unapprovedDecryptMsgCount = controller.decryptMessageManager.unapprovedDecryptMsgCount
const unapprovedEncryptionPublicKeyMsgCount = controller.encryptionPublicKeyManager.unapprovedEncryptionPublicKeyMsgCount
const unapprovedTypedMessagesCount = controller.typedMessageManager.unapprovedTypedMessagesCount
const { unapprovedMsgCount } = controller.messageManager
const { unapprovedPersonalMsgCount } = controller.personalMessageManager
const { unapprovedDecryptMsgCount } = controller.decryptMessageManager
const {
unapprovedEncryptionPublicKeyMsgCount,
} = controller.encryptionPublicKeyManager
const { unapprovedTypedMessagesCount } = controller.typedMessageManager
const count = unapprovedTxCount + unapprovedMsgCount + unapprovedPersonalMsgCount + unapprovedDecryptMsgCount + unapprovedEncryptionPublicKeyMsgCount +
unapprovedTypedMessagesCount
if (count) {
@ -452,14 +425,16 @@ function setupController (initState, initLangCode) {
*/
async function triggerUi () {
const tabs = await platform.getActiveTabs()
const currentlyActiveMetamaskTab = Boolean(tabs.find((tab) => openMetamaskTabsIDs[tab.id]))
/**
* https://github.com/poanetwork/metamask-extension/issues/19
* !notificationIsOpen was removed from the check, because notification can be opened, but it can be behind the DApp
* for some reasons. For example, if notification popup was opened, but user moved focus to DApp.
* New transaction, in this case, will not appear in front of DApp.
*/
if (!popupIsOpen && !currentlyActiveMetamaskTab) {
const currentlyActiveMetamaskTab = Boolean(
tabs.find((tab) => openMetamaskTabsIDs[tab.id]),
)
// Vivaldi is not closing port connection on popup close, so popupIsOpen does not work correctly
// To be reviewed in the future if this behaviour is fixed - also the way we determine isVivaldi variable might change at some point
const isVivaldi =
tabs.length > 0 &&
tabs[0].extData &&
tabs[0].extData.indexOf('vivaldi_tab') > -1
if ((isVivaldi || !popupIsOpen) && !currentlyActiveMetamaskTab) {
await notificationManager.showPopup()
}
}
@ -470,14 +445,12 @@ async function triggerUi () {
*/
async function openPopup () {
await triggerUi()
await new Promise(
(resolve) => {
const interval = setInterval(() => {
if (!notificationIsOpen) {
clearInterval(interval)
resolve()
}
}, 1000)
},
)
await new Promise((resolve) => {
const interval = setInterval(() => {
if (!notificationIsOpen) {
clearInterval(interval)
resolve()
}
}, 1000)
})
}

View File

@ -1,123 +1,226 @@
import querystring from 'querystring'
import pump from 'pump'
import LocalMessageDuplexStream from 'post-message-stream'
import ObjectMultiplex from 'obj-multiplex'
import extension from 'extensionizer'
import PortStream from 'extension-port-stream'
import { obj as createThoughStream } from 'through2'
// These require calls need to use require to be statically recognized by browserify
const fs = require('fs')
const path = require('path')
const pump = require('pump')
const querystring = require('querystring')
const LocalMessageDuplexStream = require('post-message-stream')
const PongStream = require('ping-pong-stream/pong')
const ObjectMultiplex = require('obj-multiplex')
const extension = require('extensionizer')
const PortStream = require('extension-port-stream')
const inpageContent = fs.readFileSync(path.join(__dirname, '..', '..', 'dist', 'chrome', 'inpage.js'), 'utf8').toString()
const inpageSuffix = '//# sourceURL=' + extension.extension.getURL('inpage.js') + '\n'
const inpageContent = fs.readFileSync(
path.join(__dirname, '..', '..', 'dist', 'chrome', 'inpage.js'),
'utf8',
)
const inpageSuffix = `//# sourceURL=${extension.runtime.getURL('inpage.js')}\n`
const inpageBundle = inpageContent + inpageSuffix
// Eventually this streaming injection could be replaced with:
// https://developer.mozilla.org/en-US/docs/Mozilla/Tech/XPCOM/Language_Bindings/Components.utils.exportFunction
//
// But for now that is only Firefox
// If we create a FireFox-only code path using that API,
// MetaMask will be much faster loading and performant on Firefox.
const CONTENT_SCRIPT = 'nifty-contentscript'
const INPAGE = 'nifty-inpage'
const PROVIDER = 'metamask-provider'
if (shouldInjectWeb3()) {
setupInjection()
// TODO:LegacyProvider: Delete
const LEGACY_CONTENT_SCRIPT = 'contentscript'
const LEGACY_INPAGE = 'inpage'
const LEGACY_PROVIDER = 'provider'
const LEGACY_PUBLIC_CONFIG = 'publicConfig'
if (shouldInjectProvider()) {
injectScript(inpageBundle)
setupStreams()
}
/**
* Creates a script tag that injects inpage.js
* Injects a script tag into the current document
*
* @param {string} content - Code to be executed in the current document
*/
function setupInjection () {
function injectScript (content) {
try {
// inject in-page script
const scriptTag = document.createElement('script')
scriptTag.textContent = inpageBundle
scriptTag.onload = function () { this.parentNode.removeChild(this) }
const container = document.head || document.documentElement
// append as first child
const scriptTag = document.createElement('script')
scriptTag.setAttribute('async', 'false')
scriptTag.textContent = content
container.insertBefore(scriptTag, container.children[0])
} catch (e) {
console.error('Nifty Wallet injection failed.', e)
container.removeChild(scriptTag)
} catch (error) {
console.error('Nifty Wallet injection failed.', error)
}
}
/**
* Sets up two-way communication streams between the
* browser extension and local per-page browser context
* browser extension and local per-page browser context.
*
*/
function setupStreams () {
// setup communication to page and plugin
async function setupStreams () {
// the transport-specific streams for communication between inpage and background
const pageStream = new LocalMessageDuplexStream({
name: 'nifty-contentscript',
target: 'nifty-inpage',
name: CONTENT_SCRIPT,
target: INPAGE,
})
const extensionPort = extension.runtime.connect({ name: CONTENT_SCRIPT })
const extensionStream = new PortStream(extensionPort)
// create and connect channel muxers
// so we can handle the channels individually
const pageMux = new ObjectMultiplex()
pageMux.setMaxListeners(25)
const extensionMux = new ObjectMultiplex()
extensionMux.setMaxListeners(25)
extensionMux.ignoreStream(LEGACY_PUBLIC_CONFIG) // TODO:LegacyProvider: Delete
pump(pageMux, pageStream, pageMux, (err) =>
logStreamDisconnectWarning('Nifty Wallet Inpage Multiplex', err),
)
pump(extensionMux, extensionStream, extensionMux, (err) => {
logStreamDisconnectWarning('Nifty Wallet Background Multiplex', err)
notifyInpageOfStreamFailure()
})
const pluginPort = extension.runtime.connect({ name: 'contentscript' })
const pluginStream = new PortStream(pluginPort)
// forward communication plugin->inpage
pump(
pageStream,
pluginStream,
extensionStream,
pageStream,
(err) => logStreamDisconnectWarning('Nifty Wallet Contentscript Forwarding', err),
)
// setup local multistream channels
const mux = new ObjectMultiplex()
mux.setMaxListeners(25)
pump(
mux,
pageStream,
mux,
(err) => logStreamDisconnectWarning('Nifty Wallet Inpage', err),
)
pump(
mux,
pluginStream,
mux,
(err) => logStreamDisconnectWarning('Nifty Wallet Background', err),
)
// connect ping stream
const pongStream = new PongStream({ objectMode: true })
pump(
mux,
pongStream,
mux,
(err) => logStreamDisconnectWarning('Nifty Wallet PingPongStream', err),
)
// connect phishing warning stream
const phishingStream = mux.createStream('phishing')
// connect "phishing" channel to warning system
const phishingStream = extensionMux.createStream('phishing')
phishingStream.once('data', redirectToPhishingWarning)
// ignore unused channels (handled by background, inpage)
mux.ignoreStream('provider')
mux.ignoreStream('publicConfig')
// TODO:LegacyProvider: Delete
// handle legacy provider
const legacyPageStream = new LocalMessageDuplexStream({
name: LEGACY_CONTENT_SCRIPT,
target: LEGACY_INPAGE,
})
const legacyPageMux = new ObjectMultiplex()
legacyPageMux.setMaxListeners(25)
const legacyExtensionMux = new ObjectMultiplex()
legacyExtensionMux.setMaxListeners(25)
pump(legacyPageMux, legacyPageStream, legacyPageMux, (err) =>
logStreamDisconnectWarning('Nifty Wallet Legacy Inpage Multiplex', err),
)
pump(
legacyExtensionMux,
extensionStream,
getNotificationTransformStream(),
legacyExtensionMux,
(err) => {
logStreamDisconnectWarning('Nifty Wallet Background Legacy Multiplex', err)
notifyInpageOfStreamFailure()
},
)
forwardNamedTrafficBetweenMuxes(
LEGACY_PROVIDER,
PROVIDER,
legacyPageMux,
legacyExtensionMux,
)
forwardTrafficBetweenMuxes(
LEGACY_PUBLIC_CONFIG,
legacyPageMux,
legacyExtensionMux,
)
}
function forwardTrafficBetweenMuxes (channelName, muxA, muxB) {
const channelA = muxA.createStream(channelName)
const channelB = muxB.createStream(channelName)
pump(channelA, channelB, channelA, (error) =>
console.debug(
`MetaMask: Muxed traffic for channel "${channelName}" failed.`,
error,
),
)
}
/**
* Error handler for page to plugin stream disconnections
*
* @param {string} remoteLabel Remote stream name
* @param {Error} err Stream connection error
*/
function logStreamDisconnectWarning (remoteLabel, err) {
let warningMsg = `MetamaskContentscript - lost connection to ${remoteLabel}`
if (err) warningMsg += '\n' + err.stack
console.warn(warningMsg)
// TODO:LegacyProvider: Delete
function forwardNamedTrafficBetweenMuxes (
channelAName,
channelBName,
muxA,
muxB,
) {
const channelA = muxA.createStream(channelAName)
const channelB = muxB.createStream(channelBName)
pump(channelA, channelB, channelA, (error) =>
console.debug(
`MetaMask: Muxed traffic between channels "${channelAName}" and "${channelBName}" failed.`,
error,
),
)
}
// TODO:LegacyProvider: Delete
function getNotificationTransformStream () {
return createThoughStream((chunk, _, cb) => {
if (chunk?.name === PROVIDER) {
if (chunk.data?.method === 'metamask_accountsChanged') {
chunk.data.method = 'wallet_accountsChanged'
chunk.data.result = chunk.data.params
delete chunk.data.params
}
}
cb(null, chunk)
})
}
/**
* Determines if Web3 should be injected
* Error handler for page to extension stream disconnections
*
* @returns {boolean} {@code true} if Web3 should be injected
* @param {string} remoteLabel - Remote stream name
* @param {Error} error - Stream connection error
*/
function shouldInjectWeb3 () {
return doctypeCheck() && suffixCheck() &&
documentElementCheck() && !blacklistedDomainCheck()
function logStreamDisconnectWarning (remoteLabel, error) {
console.debug(
`MetaMask: Content script lost connection to "${remoteLabel}".`,
error,
)
}
/**
* This function must ONLY be called in pump destruction/close callbacks.
* Notifies the inpage context that streams have failed, via window.postMessage.
* Relies on obj-multiplex and post-message-stream implementation details.
*/
function notifyInpageOfStreamFailure () {
window.postMessage(
{
target: INPAGE, // the post-message-stream "target"
data: {
// this object gets passed to obj-multiplex
name: PROVIDER, // the obj-multiplex channel name
data: {
jsonrpc: '2.0',
method: 'METAMASK_STREAM_FAILURE',
},
},
},
window.location.origin,
)
}
/**
* Determines if the provider should be injected
*
* @returns {boolean} {@code true} Whether the provider should be injected
*/
function shouldInjectProvider () {
return (
doctypeCheck() &&
suffixCheck() &&
documentElementCheck() &&
!blockedDomainCheck()
)
}
/**
@ -126,28 +229,24 @@ function shouldInjectWeb3 () {
* @returns {boolean} {@code true} if the doctype is html or if none exists
*/
function doctypeCheck () {
const doctype = window.document.doctype
const { doctype } = window.document
if (doctype) {
return doctype.name === 'html'
} else {
return true
}
return true
}
/**
* Returns whether or not the extension (suffix) of the current document is prohibited
*
* This checks {@code window.location.pathname} against a set of file extensions
* that should not have web3 injected into them. This check is indifferent of query parameters
* in the location.
* that we should not inject the provider into. This check is indifferent of
* query parameters in the location.
*
* @returns {boolean} whether or not the extension of the current document is prohibited
*/
function suffixCheck () {
const prohibitedTypes = [
/\.xml$/,
/\.pdf$/,
]
const prohibitedTypes = [/\.xml$/u, /\.pdf$/u]
const currentUrl = window.location.pathname
for (let i = 0; i < prohibitedTypes.length; i++) {
if (prohibitedTypes[i].test(currentUrl)) {
@ -171,12 +270,12 @@ function documentElementCheck () {
}
/**
* Checks if the current domain is blacklisted
* Checks if the current domain is blocked
*
* @returns {boolean} {@code true} if the current domain is blacklisted
* @returns {boolean} {@code true} if the current domain is blocked
*/
function blacklistedDomainCheck () {
const blacklistedDomains = [
function blockedDomainCheck () {
const blockedDomains = [
'uscourts.gov',
'dropbox.com',
'webbyawards.com',
@ -186,12 +285,16 @@ function blacklistedDomainCheck () {
'harbourair.com',
'ani.gamer.com.tw',
'blueskybooking.com',
'sharefile.com',
]
const currentUrl = window.location.href
let currentRegex
for (let i = 0; i < blacklistedDomains.length; i++) {
const blacklistedDomain = blacklistedDomains[i].replace('.', '\\.')
currentRegex = new RegExp(`(?:https?:\\/\\/)(?:(?!${blacklistedDomain}).)*$`)
for (let i = 0; i < blockedDomains.length; i++) {
const blockedDomain = blockedDomains[i].replace('.', '\\.')
currentRegex = new RegExp(
`(?:https?:\\/\\/)(?:(?!${blockedDomain}).)*$`,
'u',
)
if (!currentRegex.test(currentUrl)) {
return true
}

View File

@ -1,4 +1,4 @@
const ObservableStore = require('obs-store')
import { ObservableStore } from '@metamask/obs-store'
const extend = require('xtend')
class AddressBookController {

View File

@ -1,4 +1,4 @@
import ObservableStore from 'obs-store'
import { ObservableStore } from '@metamask/obs-store'
import PendingBalanceCalculator from '../lib/pending-balance-calculator'
import { BN } from 'ethereumjs-util'

View File

@ -1,4 +1,4 @@
const ObservableStore = require('obs-store')
import { ObservableStore } from '@metamask/obs-store'
const extend = require('xtend')
const PhishingDetector = require('eth-phishing-detect/src/detector')
const log = require('loglevel')

View File

@ -1,4 +1,4 @@
const ObservableStore = require('obs-store')
import { ObservableStore } from '@metamask/obs-store'
const extend = require('xtend')
/**

View File

@ -1,4 +1,4 @@
const ObservableStore = require('obs-store')
import { ObservableStore } from '@metamask/obs-store'
const extend = require('xtend')
const BalanceController = require('./balance')

View File

@ -1,4 +1,4 @@
const ObservableStore = require('obs-store')
import { ObservableStore } from '@metamask/obs-store'
const extend = require('xtend')
const log = require('loglevel')

View File

@ -1,5 +1,5 @@
import Web3 from 'web3'
import contractsETH from 'eth-contract-metadata'
import contractsETH from '@metamask/contract-metadata'
import contractsPOA from 'poa-contract-metadata'
import contractsRSK from '@rsksmart/rsk-contract-metadata'
import contractsRSKTest from '@rsksmart/rsk-testnet-contract-metadata'

View File

@ -1,42 +0,0 @@
const ObservableStore = require('obs-store')
const extend = require('xtend')
const log = require('loglevel')
// every ten minutes
const POLLING_INTERVAL = 10 * 60 * 1000
class InfuraController {
constructor (opts = {}) {
const initState = extend({
infuraNetworkStatus: {},
}, opts.initState)
this.store = new ObservableStore(initState)
}
//
// PUBLIC METHODS
//
// Responsible for retrieving the status of Infura's nodes. Can return either
// ok, degraded, or down.
async checkInfuraNetworkStatus () {
const response = await fetch('https://api.infura.io/v1/status/metamask')
const parsedResponse = await response.json()
this.store.updateState({
infuraNetworkStatus: parsedResponse,
})
return parsedResponse
}
scheduleInfuraNetworkCheck () {
if (this.conversionInterval) {
clearInterval(this.conversionInterval)
}
this.conversionInterval = setInterval(() => {
this.checkInfuraNetworkStatus().catch(log.warn)
}, POLLING_INTERVAL)
}
}
module.exports = InfuraController

View File

@ -1,5 +1,4 @@
import mergeMiddleware from 'json-rpc-engine/src/mergeMiddleware'
import createScaffoldMiddleware from 'json-rpc-engine/src/createScaffoldMiddleware'
import { createScaffoldMiddleware, mergeMiddleware} from 'json-rpc-engine'
import createBlockReRefMiddleware from 'eth-json-rpc-middleware/block-ref'
import createRetryOnEmptyMiddleware from 'eth-json-rpc-middleware/retryOnEmpty'
import createBlockCacheMiddleware from 'eth-json-rpc-middleware/block-cache'
@ -11,8 +10,13 @@ import BlockTracker from 'eth-block-tracker'
export default createInfuraClient
function createInfuraClient ({ network }) {
const infuraMiddleware = createInfuraMiddleware({ network, maxAttempts: 5, source: 'metamask' })
function createInfuraClient ({ network, projectId }) {
const infuraMiddleware = createInfuraMiddleware({
network,
projectId,
maxAttempts: 5,
source: 'metamask',
})
const infuraProvider = providerFromMiddleware(infuraMiddleware)
const blockTracker = new BlockTracker({ provider: infuraProvider })

View File

@ -1,4 +1,4 @@
import mergeMiddleware from 'json-rpc-engine/src/mergeMiddleware'
import { mergeMiddleware } from 'json-rpc-engine'
import createFetchMiddleware from 'eth-json-rpc-middleware/fetch'
import createBlockRefRewriteMiddleware from 'eth-json-rpc-middleware/block-ref-rewrite'
import createBlockCacheMiddleware from 'eth-json-rpc-middleware/block-cache'

View File

@ -1,8 +1,7 @@
import mergeMiddleware from 'json-rpc-engine/src/mergeMiddleware'
import { createAsyncMiddleware, mergeMiddleware } from 'json-rpc-engine'
import createFetchMiddleware from 'eth-json-rpc-middleware/fetch'
import createBlockRefRewriteMiddleware from 'eth-json-rpc-middleware/block-ref-rewrite'
import createBlockTrackerInspectorMiddleware from 'eth-json-rpc-middleware/block-tracker-inspector'
import createAsyncMiddleware from 'json-rpc-engine/src/createAsyncMiddleware'
import providerFromMiddleware from 'eth-json-rpc-middleware/providerFromMiddleware'
import BlockTracker from 'eth-block-tracker'

View File

@ -1,5 +1,4 @@
import mergeMiddleware from 'json-rpc-engine/src/mergeMiddleware'
import createScaffoldMiddleware from 'json-rpc-engine/src/createScaffoldMiddleware'
import { createScaffoldMiddleware, mergeMiddleware } from 'json-rpc-engine'
import createWalletSubprovider from 'eth-json-rpc-middleware/wallet'
import { createPendingNonceMiddleware } from './middleware/pending'

View File

@ -1,5 +1,4 @@
const mergeMiddleware = require('json-rpc-engine/src/mergeMiddleware')
const createScaffoldMiddleware = require('json-rpc-engine/src/createScaffoldMiddleware')
const { createScaffoldMiddleware, mergeMiddleware } = require('json-rpc-engine')
const createBlockReRefMiddleware = require('eth-json-rpc-middleware/block-ref')
const createRetryOnEmptyMiddleware = require('eth-json-rpc-middleware/retryOnEmpty')
const createBlockCacheMiddleware = require('eth-json-rpc-middleware/block-cache')

View File

@ -1,15 +1,17 @@
const { formatTxMetaForRpcResult } = require('../util')
import createAsyncMiddleware from 'json-rpc-engine/src/createAsyncMiddleware'
import { createAsyncMiddleware } from 'json-rpc-engine'
import { formatTxMetaForRpcResult } from '../util'
export function createPendingNonceMiddleware ({ getPendingNonce }) {
return createAsyncMiddleware(async (req, res, next) => {
const { method, params } = req
if (method !== 'eth_getTransactionCount') {
return next()
next()
return
}
const [param, blockRef] = params
if (blockRef !== 'pending') {
return next()
next()
return
}
res.result = await getPendingNonce(param)
})
@ -19,12 +21,14 @@ export function createPendingTxMiddleware ({ getPendingTransactionByHash }) {
return createAsyncMiddleware(async (req, res, next) => {
const { method, params } = req
if (method !== 'eth_getTransactionByHash') {
return next()
next()
return
}
const [hash] = params
const txMeta = getPendingTransactionByHash(hash)
if (!txMeta) {
return next()
next()
return
}
res.result = formatTxMetaForRpcResult(txMeta)
})

View File

@ -1,9 +1,8 @@
import assert from 'assert'
import EventEmitter from 'events'
import ObservableStore from 'obs-store'
import ComposedStore from 'obs-store/lib/composed'
import { ComposedStore, ObservableStore } from '@metamask/obs-store'
import EthQuery from 'eth-query'
import JsonRpcEngine from 'json-rpc-engine'
import { JsonRpcEngine } from 'json-rpc-engine'
import providerFromEngine from 'eth-json-rpc-middleware/providerFromEngine'
import log from 'loglevel'
import createMetamaskMiddleware from './createMetamaskMiddleware'
@ -87,6 +86,21 @@ module.exports = class NetworkController extends EventEmitter {
this._blockTrackerProxy = null
}
/**
* Sets the Infura project ID
*
* @param {string} projectId - The Infura project ID
* @throws {Error} if the project ID is not a valid string
* @return {void}
*/
setInfuraProjectId (projectId) {
if (!projectId || typeof projectId !== 'string') {
throw new Error('Invalid Infura project ID')
}
this._infuraProjectId = projectId
}
initializeProvider (providerParams) {
this._baseProviderParams = providerParams
const { type, rpcTarget, chainId, ticker, nickname } = this.providerStore.getState()
@ -232,7 +246,7 @@ module.exports = class NetworkController extends EventEmitter {
if (isPocket && this.dProviderStore.getState().dProvider) {
this._configurePocketProvider(opts)
} else if (isInfura) {
this._configureInfuraProvider(opts)
this._configureInfuraProvider(type, this._infuraProjectId)
// other type-based rpc endpoints
} else if (type === MAINNET) {
this._configureStandardProvider({ rpcUrl: this._ethMainnetRpcEndpoint, chainId, ticker, nickname })
@ -275,17 +289,13 @@ module.exports = class NetworkController extends EventEmitter {
this._ethMainnetRpcEndpoint = endpoint
}
_configureInfuraProvider ({ type }) {
_configureInfuraProvider (type, projectId) {
log.info('NetworkController - configureInfuraProvider', type)
const networkClient = createInfuraClient({
network: type,
projectId,
})
this._setNetworkClient(networkClient)
// setup networkConfig
const settings = {
ticker: 'ETH',
}
this.networkConfig.putState(settings)
}
_configurePocketProvider ({ type }) {

View File

@ -171,20 +171,22 @@ const getNetworkDisplayName = key => networks[key].displayName
function formatTxMetaForRpcResult (txMeta) {
return {
'blockHash': txMeta.txReceipt ? txMeta.txReceipt.blockHash : null,
'blockNumber': txMeta.txReceipt ? txMeta.txReceipt.blockNumber : null,
'from': txMeta.txParams.from,
'gas': txMeta.txParams.gas,
'gasPrice': txMeta.txParams.gasPrice,
'hash': txMeta.hash,
'input': txMeta.txParams.data || '0x',
'nonce': txMeta.txParams.nonce,
'to': txMeta.txParams.to,
'transactionIndex': txMeta.txReceipt ? txMeta.txReceipt.transactionIndex : null,
'value': txMeta.txParams.value || '0x0',
'v': txMeta.v,
'r': txMeta.r,
's': txMeta.s,
blockHash: txMeta.txReceipt ? txMeta.txReceipt.blockHash : null,
blockNumber: txMeta.txReceipt ? txMeta.txReceipt.blockNumber : null,
from: txMeta.txParams.from,
gas: txMeta.txParams.gas,
gasPrice: txMeta.txParams.gasPrice,
hash: txMeta.hash,
input: txMeta.txParams.data || '0x',
nonce: txMeta.txParams.nonce,
to: txMeta.txParams.to,
transactionIndex: txMeta.txReceipt
? txMeta.txReceipt.transactionIndex
: null,
value: txMeta.txParams.value || '0x0',
v: txMeta.v,
r: txMeta.r,
s: txMeta.s,
}
}

View File

@ -1,3 +1,4 @@
export const APPROVAL_TYPE = 'wallet_requestPermissions'
export const WALLET_PREFIX = 'wallet_'
@ -7,16 +8,27 @@ export const LOG_STORE_KEY = 'permissionsLog'
export const METADATA_STORE_KEY = 'domainMetadata'
export const METADATA_CACHE_MAX_SIZE = 100
export const CAVEAT_NAMES = {
exposedAccounts: 'exposedAccounts',
primaryAccountOnly: 'primaryAccountOnly',
}
export const CAVEAT_TYPES = {
limitResponseLength: 'limitResponseLength',
filterResponse: 'filterResponse',
}
export const NOTIFICATION_NAMES = {
accountsChanged: 'wallet_accountsChanged',
accountsChanged: 'metamask_accountsChanged',
unlockStateChanged: 'metamask_unlockStateChanged',
chainChanged: 'metamask_chainChanged',
}
export const LOG_IGNORE_METHODS = [
'wallet_sendDomainMetadata',
'wallet_registerOnboarding',
'wallet_watchAsset',
]
export const LOG_METHOD_TYPES = {
@ -27,14 +39,11 @@ export const LOG_METHOD_TYPES = {
export const LOG_LIMIT = 100
export const SAFE_METHODS = [
'web3_sha3',
'net_listening',
'net_peerCount',
'net_version',
'eth_blockNumber',
'eth_call',
'eth_chainId',
'eth_coinbase',
'eth_decrypt',
'eth_estimateGas',
'eth_gasPrice',
'eth_getBalance',
@ -43,9 +52,11 @@ export const SAFE_METHODS = [
'eth_getBlockTransactionCountByHash',
'eth_getBlockTransactionCountByNumber',
'eth_getCode',
'eth_getEncryptionPublicKey',
'eth_getFilterChanges',
'eth_getFilterLogs',
'eth_getLogs',
'eth_getProof',
'eth_getStorageAt',
'eth_getTransactionByBlockHashAndIndex',
'eth_getTransactionByBlockNumberAndIndex',
@ -66,8 +77,6 @@ export const SAFE_METHODS = [
'eth_sendRawTransaction',
'eth_sendTransaction',
'eth_sign',
'personal_sign',
'personal_ecRecover',
'eth_signTypedData',
'eth_signTypedData_v1',
'eth_signTypedData_v3',
@ -76,9 +85,14 @@ export const SAFE_METHODS = [
'eth_submitWork',
'eth_syncing',
'eth_uninstallFilter',
'metamask_getProviderState',
'metamask_watchAsset',
'net_listening',
'net_peerCount',
'net_version',
'personal_ecRecover',
'personal_sign',
'wallet_watchAsset',
'eth_getEncryptionPublicKey',
'eth_decrypt',
'eth_accounts',
'web3_clientVersion',
'web3_sha3',
]

View File

@ -1,6 +1,5 @@
import JsonRpcEngine from 'json-rpc-engine'
import asMiddleware from 'json-rpc-engine/src/asMiddleware'
import ObservableStore from 'obs-store'
import { JsonRpcEngine } from 'json-rpc-engine'
import { ObservableStore } from '@metamask/obs-store'
import log from 'loglevel'
import { CapabilitiesController as RpcCap } from 'rpc-cap'
import { ethErrors } from 'eth-json-rpc-errors'
@ -90,7 +89,7 @@ export class PermissionsController {
this.permissions, { origin },
))
return asMiddleware(engine)
return engine.asMiddleware(engine)
}
/**

View File

@ -1,4 +1,4 @@
import createAsyncMiddleware from 'json-rpc-engine/src/createAsyncMiddleware'
import { createAsyncMiddleware } from 'json-rpc-engine'
import { ethErrors } from 'eth-json-rpc-errors'
/**

View File

@ -1,4 +1,4 @@
const ObservableStore = require('obs-store')
import { ObservableStore } from '@metamask/obs-store'
const normalizeAddress = require('eth-sig-util').normalize
const { isValidAddress } = require('ethereumjs-util')
const extend = require('xtend')

View File

@ -1,4 +1,4 @@
const ObservableStore = require('obs-store')
import { ObservableStore } from '@metamask/obs-store'
const extend = require('xtend')
const EthQuery = require('eth-query')
const log = require('loglevel')

View File

@ -1,4 +1,4 @@
const ObservableStore = require('obs-store')
import { ObservableStore } from '@metamask/obs-store'
const extend = require('xtend')
const log = require('loglevel')

View File

@ -1,4 +1,4 @@
const ObservableStore = require('obs-store')
import { ObservableStore } from '@metamask/obs-store'
import log from 'loglevel'
import { normalize as normalizeAddress } from 'eth-sig-util'

View File

@ -1,9 +1,9 @@
import EventEmitter from 'safe-event-emitter'
import ObservableStore from 'obs-store'
import { ObservableStore } from '@metamask/obs-store'
import ethUtil from 'ethereumjs-util'
import Transaction from 'ethereumjs-tx'
import EthQuery from 'ethjs-query'
import { ethErrors } from 'eth-json-rpc-errors'
import abi from 'human-standard-token-abi'
import abiDecoder from 'abi-decoder'
@ -14,14 +14,15 @@ import TxGasUtil from './tx-gas-utils'
const PendingTransactionTracker = require('./pending-tx-tracker')
import NonceTracker from 'nonce-tracker'
import * as txUtils from './lib/util'
import {
TRANSACTION_STATUSES,
TRANSACTION_TYPES,
} from '../../../../shared/constants/transaction'
import cleanErrorStack from '../../lib/cleanErrorStack'
import log from 'loglevel'
const recipientBlacklistChecker = require('./lib/recipient-blacklist-checker')
const {
TRANSACTION_TYPE_CANCEL,
TRANSACTION_TYPE_RETRY,
TRANSACTION_TYPE_STANDARD,
TRANSACTION_STATUS_APPROVED,
} = require('./enums')
const { hexToBn, bnToHex, BnMultiplyByFraction } = require('../../lib/util')
@ -40,18 +41,17 @@ const { hexToBn, bnToHex, BnMultiplyByFraction } = require('../../lib/util')
<br>- nonceTracker
calculating nonces
@class
@param {object} - opts
@param {object} opts.initState - initial transaction list default is an empty array
@param {Object} opts.networkStore - an observable store for network number
@param {Object} opts.blockTracker - An instance of eth-blocktracker
@param {Object} opts.provider - A network provider.
@param {Function} opts.signTransaction - function the signs an ethereumjs-tx
@param {Function} [opts.getGasPrice] - optional gas price calculator
@param {Function} opts.signTransaction - ethTx signer that returns a rawTx
@param {Number} [opts.txHistoryLimit] - number *optional* for limiting how many transactions are in state
@param {Object} opts.preferencesStore
@param {Object} opts
@param {Object} opts.initState - initial transaction list default is an empty array
@param {Object} opts.networkStore - an observable store for network number
@param {Object} opts.blockTracker - An instance of eth-blocktracker
@param {Object} opts.provider - A network provider.
@param {Function} opts.signTransaction - function the signs an ethereumjs-tx
@param {Object} opts.getPermittedAccounts - get accounts that an origin has permissions for
@param {Function} opts.signTransaction - ethTx signer that returns a rawTx
@param {number} [opts.txHistoryLimit] - number *optional* for limiting how many transactions are in state
@param {Object} opts.preferencesStore
*/
class TransactionController extends EventEmitter {
@ -104,7 +104,12 @@ class TransactionController extends EventEmitter {
this._updatePendingTxsAfterFirstBlock()
}
/** @returns {number} the chainId*/
/**
* Gets the current chainId in the network store as a number, returning 0 if
* the chainId parses to NaN.
*
* @returns {number} The numerical chainId.
*/
getChainId () {
const networkState = this.networkStore.getState()
const getChainId = parseInt(networkState)
@ -115,10 +120,10 @@ class TransactionController extends EventEmitter {
}
}
/**
/**
Adds a tx to the txlist
@emits ${txMeta.id}:unapproved
*/
*/
addTx (txMeta) {
this.txStateManager.addTx(txMeta)
this.emit(`${txMeta.id}:unapproved`, txMeta)
@ -133,42 +138,66 @@ class TransactionController extends EventEmitter {
}
/**
add a new unapproved transaction to the pipeline
@returns {Promise<string>} the hash of the transaction after being submitted to the network
@param txParams {object} - txParams for the transaction
@param opts {object} - with the key origin to put the origin on the txMeta
*/
* Add a new unapproved transaction to the pipeline
*
* @returns {Promise<string>} the hash of the transaction after being submitted to the network
* @param {Object} txParams - txParams for the transaction
* @param {Object} opts - with the key origin to put the origin on the txMeta
*/
async newUnapprovedTransaction (txParams, opts = {}) {
log.debug(`MetaMaskController newUnapprovedTransaction ${JSON.stringify(txParams)}`)
const initialTxMeta = await this.addUnapprovedTransaction(txParams)
initialTxMeta.origin = opts.origin
this.txStateManager.updateTx(initialTxMeta, '#newUnapprovedTransaction - adding the origin')
log.debug(
`MetaMaskController newUnapprovedTransaction ${JSON.stringify(txParams)}`,
)
const initialTxMeta = await this.addUnapprovedTransaction(
txParams,
opts.origin,
)
// listen for tx completion (success, fail)
return new Promise((resolve, reject) => {
this.txStateManager.once(`${initialTxMeta.id}:finished`, (finishedTxMeta) => {
switch (finishedTxMeta.status) {
case 'submitted':
return resolve(finishedTxMeta.hash)
case 'rejected':
return reject(cleanErrorStack(new Error('Nifty Wallet Tx Signature: User denied transaction signature.')))
case 'failed':
return reject(cleanErrorStack(new Error(finishedTxMeta.err.message)))
default:
return reject(cleanErrorStack(new Error(`Nifty Wallet Tx Signature: Unknown problem: ${JSON.stringify(finishedTxMeta.txParams)}`)))
}
})
this.txStateManager.once(
`${initialTxMeta.id}:finished`,
(finishedTxMeta) => {
switch (finishedTxMeta.status) {
case TRANSACTION_STATUSES.SUBMITTED:
return resolve(finishedTxMeta.hash)
case TRANSACTION_STATUSES.REJECTED:
return reject(
cleanErrorStack(
ethErrors.provider.userRejectedRequest(
'MetaMask Tx Signature: User denied transaction signature.',
),
),
)
case TRANSACTION_STATUSES.FAILED:
return reject(
cleanErrorStack(
ethErrors.rpc.internal(finishedTxMeta.err.message),
),
)
default:
return reject(
cleanErrorStack(
ethErrors.rpc.internal(
`MetaMask Tx Signature: Unknown problem: ${JSON.stringify(
finishedTxMeta.txParams,
)}`,
),
),
)
}
},
)
})
}
/**
Validates and generates a txMeta with defaults and puts it in txStateManager
store
@returns {txMeta}
*/
* Validates and generates a txMeta with defaults and puts it in txStateManager
* store.
*
* @returns {txMeta}
*/
async addUnapprovedTransaction (txParams) {
// validate
const normalizedTxParams = txUtils.normalizeTxParams(txParams)
@ -180,7 +209,7 @@ class TransactionController extends EventEmitter {
// construct txMeta
let txMeta = this.txStateManager.generateTxMeta({
txParams: normalizedTxParams,
type: TRANSACTION_TYPE_STANDARD,
type: TRANSACTION_TYPES.STANDARD,
})
this.addTx(txMeta)
this.emit('newUnapprovedTx', txMeta)
@ -201,11 +230,12 @@ class TransactionController extends EventEmitter {
return txMeta
}
/**
adds the tx gas defaults: gas && gasPrice
@param txMeta {Object} - the txMeta object
@returns {Promise<object>} resolves with txMeta
*/
/**
* Adds the tx gas defaults: gas && gasPrice
* @param {Object} txMeta - the txMeta object
* @returns {Promise<object>} resolves with txMeta
*/
async addTxGasDefaults (txMeta) {
const txParams = txMeta.txParams
// ensure value
@ -247,7 +277,7 @@ class TransactionController extends EventEmitter {
* new transaction contains the same nonce as the previous, is a basic ETH transfer of 0x value to
* the sender's address, and has a higher gasPrice than that of the previous transaction.
* @param {number} originalTxId - the id of the txMeta that you want to attempt to cancel
* @param {string=} customGasPrice - the hex value to use for the cancel transaction
* @param {string} [customGasPrice] - the hex value to use for the cancel transaction
* @returns {txMeta}
*/
async createCancelTransaction (originalTxId, customGasPrice) {
@ -267,8 +297,8 @@ class TransactionController extends EventEmitter {
},
lastGasPrice,
loadingDefaults: false,
status: TRANSACTION_STATUS_APPROVED,
type: TRANSACTION_TYPE_CANCEL,
status: TRANSACTION_STATUSES.APPROVED,
type: TRANSACTION_TYPES.CANCEL,
})
this.addTx(newTxMeta)
@ -278,7 +308,7 @@ class TransactionController extends EventEmitter {
/**
updates the txMeta in the txStateManager
@param txMeta {Object} - the updated txMeta
@param {Object} txMeta - the updated txMeta
*/
async updateTransaction (txMeta) {
this.txStateManager.updateTx(txMeta, 'confTx: user updated transaction')
@ -286,7 +316,7 @@ class TransactionController extends EventEmitter {
/**
updates and approves the transaction
@param txMeta {Object}
@param {Object} txMeta
*/
async updateAndApproveTransaction (txMeta) {
this.txStateManager.updateTx(txMeta, 'confTx: user approved transaction')
@ -300,7 +330,7 @@ class TransactionController extends EventEmitter {
signs the transaction
publishes the transaction
if any of these steps fails the tx status will be set to failed
@param txId {number} - the tx's Id
@param {number} txId - the tx's Id
*/
async approveTransaction (txId, customNonce) {
let nonceLock
@ -338,10 +368,11 @@ class TransactionController extends EventEmitter {
throw err
}
}
/**
/**
adds the chain id and signs the transaction and set the status to signed
@param txId {number} - the tx's Id
@returns - rawTx {string}
@param {number} txId - the tx's Id
@returns {string} rawTx
*/
async signTransaction (txId) {
const txMeta = this.txStateManager.getTx(txId)
@ -360,8 +391,8 @@ class TransactionController extends EventEmitter {
/**
publishes the raw tx and sets the txMeta to submitted
@param txId {number} - the tx's Id
@param rawTx {string} - the hex string of the serialized signed transaction
@param {number} txId - the tx's Id
@param {string} rawTx - the hex string of the serialized signed transaction
@returns {Promise<void>}
*/
async publishTransaction (txId, rawTx) {
@ -413,7 +444,7 @@ class TransactionController extends EventEmitter {
/**
Convenience method for the ui thats sets the transaction to rejected
@param txId {number} - the tx's Id
@param {number} txId - the tx's Id
@returns {Promise<void>}
*/
async cancelTransaction (txId) {
@ -422,8 +453,8 @@ class TransactionController extends EventEmitter {
/**
Sets the txHas on the txMeta
@param txId {number} - the tx's Id
@param txHash {string} - the hash for the txMeta
@param {number} txId - the tx's Id
@param {string} txHash - the hash for the txMeta
*/
setTxHash (txId, txHash) {
// Add the tx hash to the persisted meta-tx object
@ -469,57 +500,96 @@ class TransactionController extends EventEmitter {
*/
_onBootCleanUp () {
this.txStateManager.getFilteredTxList({
status: 'unapproved',
this.txStateManager
.getFilteredTxList({
status: TRANSACTION_STATUSES.UNAPPROVED,
loadingDefaults: true,
}).forEach((tx) => {
})
.forEach((tx) => {
this.addTxGasDefaults(tx)
.then((txMeta) => {
txMeta.loadingDefaults = false
this.txStateManager.updateTx(txMeta, 'transactions: gas estimation for tx on boot')
}).catch((error) => {
this.txStateManager.setTxStatusFailed(tx.id, error)
})
.then((txMeta) => {
txMeta.loadingDefaults = false
this.txStateManager.updateTx(
txMeta,
'transactions: gas estimation for tx on boot',
)
})
.catch((error) => {
const txMeta = this.txStateManager.getTx(tx.id)
txMeta.loadingDefaults = false
this.txStateManager.updateTx(
txMeta,
'failed to estimate gas during boot cleanup.',
)
this.txStateManager.setTxStatusFailed(txMeta.id, error)
})
})
this.txStateManager.getFilteredTxList({
status: TRANSACTION_STATUS_APPROVED,
}).forEach((txMeta) => {
const txSignError = new Error('Transaction found as "approved" during boot - possibly stuck during signing')
this.txStateManager
.getFilteredTxList({
status: TRANSACTION_STATUSES.APPROVED,
})
.forEach((txMeta) => {
const txSignError = new Error(
'Transaction found as "approved" during boot - possibly stuck during signing',
)
this.txStateManager.setTxStatusFailed(txMeta.id, txSignError)
})
}
}
/**
is called in constructor applies the listeners for pendingTxTracker txStateManager
and blockTracker
*/
_setupListeners () {
this.txStateManager.on('tx:status-update', this.emit.bind(this, 'tx:status-update'))
this._setupBlockTrackerListener()
this.pendingTxTracker.on('tx:warning', (txMeta) => {
this.txStateManager.updateTx(txMeta, 'transactions/pending-tx-tracker#event: tx:warning')
})
this.pendingTxTracker.on('tx:failed', this.txStateManager.setTxStatusFailed.bind(this.txStateManager))
this.pendingTxTracker.on('tx:confirmed', (txId) => this.confirmTransaction(txId))
this.pendingTxTracker.on('tx:block-update', (txMeta, latestBlockNumber) => {
if (!txMeta.firstRetryBlockNumber) {
txMeta.firstRetryBlockNumber = latestBlockNumber
this.txStateManager.updateTx(txMeta, 'transactions/pending-tx-tracker#event: tx:block-update')
}
})
this.pendingTxTracker.on('tx:retry', (txMeta) => {
if (!('retryCount' in txMeta)) txMeta.retryCount = 0
txMeta.retryCount++
this.txStateManager.updateTx(txMeta, 'transactions/pending-tx-tracker#event: tx:retry')
})
}
_setupListeners () {
this.txStateManager.on(
'tx:status-update',
this.emit.bind(this, 'tx:status-update'),
)
this._setupBlockTrackerListener()
this.pendingTxTracker.on('tx:warning', (txMeta) => {
this.txStateManager.updateTx(
txMeta,
'transactions/pending-tx-tracker#event: tx:warning',
)
})
this.pendingTxTracker.on(
'tx:failed',
this.txStateManager.setTxStatusFailed.bind(this.txStateManager),
)
this.pendingTxTracker.on('tx:confirmed', (txId, transactionReceipt) =>
this.confirmTransaction(txId, transactionReceipt),
)
this.pendingTxTracker.on(
'tx:dropped',
this.txStateManager.setTxStatusDropped.bind(this.txStateManager),
)
this.pendingTxTracker.on('tx:block-update', (txMeta, latestBlockNumber) => {
if (!txMeta.firstRetryBlockNumber) {
txMeta.firstRetryBlockNumber = latestBlockNumber
this.txStateManager.updateTx(
txMeta,
'transactions/pending-tx-tracker#event: tx:block-update',
)
}
})
this.pendingTxTracker.on('tx:retry', (txMeta) => {
if (!('retryCount' in txMeta)) {
txMeta.retryCount = 0
}
txMeta.retryCount += 1
this.txStateManager.updateTx(
txMeta,
'transactions/pending-tx-tracker#event: tx:retry',
)
})
}
/**
Sets other txMeta statuses to dropped if the txMeta that has been confirmed has other transactions
in the list have the same nonce
@param txId {Number} - the txId of the transaction that has been confirmed in a block
@param {number} txId - the txId of the transaction that has been confirmed in a block
*/
_markNonceDuplicatesDropped (txId) {
// get the confirmed transactions nonce and from address
@ -540,8 +610,7 @@ class TransactionController extends EventEmitter {
_setupBlockTrackerListener () {
let listenersAreActive = false
const latestBlockHandler = this._onLatestBlock.bind(this)
const blockTracker = this.blockTracker
const txStateManager = this.txStateManager
const { blockTracker, txStateManager } = this
txStateManager.on('tx:status-update', updateSubscription)
updateSubscription()

View File

@ -1,23 +1,15 @@
import jsonDiffer from 'fast-json-patch'
import { cloneDeep } from 'lodash'
/** @module*/
export default {
generateHistoryEntry,
replayHistory,
snapshotFromTxMeta,
migrateFromSnapshotsToDiffs,
}
/**
converts non-initial history entries into diffs
@param {array} longHistory
@returns {array}
@param {Array} longHistory
@returns {Array}
*/
function migrateFromSnapshotsToDiffs (longHistory) {
export function migrateFromSnapshotsToDiffs (longHistory) {
return (
longHistory
// convert non-initial history entries into diffs
// convert non-initial history entries into diffs
.map((entry, index) => {
if (index === 0) {
return entry
@ -31,15 +23,15 @@ function migrateFromSnapshotsToDiffs (longHistory) {
Generates an array of history objects sense the previous state.
The object has the keys
op (the operation performed),
path (the key and if a nested object then each key will be seperated with a `/`)
path (the key and if a nested object then each key will be separated with a `/`)
value
with the first entry having the note and a timestamp when the change took place
@param {Object} previousState - the previous state of the object
@param {Object} newState - the update object
@param {string} [note] - a optional note for the state change
@returns {array}
@returns {Array}
*/
function generateHistoryEntry (previousState, newState, note) {
export function generateHistoryEntry (previousState, newState, note) {
const entry = jsonDiffer.compare(previousState, newState)
// Add a note to the first op, since it breaks if we append it to the entry
if (entry[0]) {
@ -56,19 +48,20 @@ function generateHistoryEntry (previousState, newState, note) {
Recovers previous txMeta state obj
@returns {Object}
*/
function replayHistory (_shortHistory) {
export function replayHistory (_shortHistory) {
const shortHistory = cloneDeep(_shortHistory)
return shortHistory.reduce((val, entry) => jsonDiffer.applyPatch(val, entry).newDocument)
return shortHistory.reduce(
(val, entry) => jsonDiffer.applyPatch(val, entry).newDocument,
)
}
/**
@param {Object} txMeta
@returns {Object} - a clone object of the txMeta with out history
*/
function snapshotFromTxMeta (txMeta) {
// create txMeta snapshot for history
const snapshot = cloneDeep(txMeta)
// dont include previous history in this snapshot
delete snapshot.history
return snapshot
* Snapshot {@code txMeta}
* @param {Object} txMeta - the tx metadata object
* @returns {Object} a deep clone without history
*/
export function snapshotFromTxMeta (txMeta) {
const shallow = { ...txMeta }
delete shallow.history
return cloneDeep(shallow)
}

View File

@ -1,6 +1,6 @@
const EventEmitter = require('events')
const log = require('loglevel')
const EthQuery = require('ethjs-query')
import EventEmitter from 'safe-event-emitter'
import log from 'loglevel'
import EthQuery from 'ethjs-query'
/**

View File

@ -1,82 +1,110 @@
import EventEmitter from 'safe-event-emitter'
import ObservableStore from 'obs-store'
import { ObservableStore } from '@metamask/obs-store'
import log from 'loglevel'
import txStateHistoryHelper from './lib/tx-state-history-helper'
import createId from '../../lib/random-id'
import { TRANSACTION_STATUSES } from '../../../../shared/constants/transaction'
import {
generateHistoryEntry,
replayHistory,
snapshotFromTxMeta,
} from './lib/tx-state-history-helpers'
import { getFinalStates, normalizeTxParams } from './lib/util'
/**
TransactionStateManager is responsible for the state of a transaction and
storing the transaction
it also has some convenience methods for finding subsets of transactions
*
*STATUS METHODS
<br>statuses:
<br> - `'unapproved'` the user has not responded
<br> - `'rejected'` the user has responded no!
<br> - `'approved'` the user has approved the tx
<br> - `'signed'` the tx is signed
<br> - `'submitted'` the tx is sent to a server
<br> - `'confirmed'` the tx has been included in a block.
<br> - `'failed'` the tx failed for some reason, included on tx data.
<br> - `'dropped'` the tx nonce was already used
@param {Object} opts
@param {Object} [opts.initState={ transactions: [] }] initial transactions list with the key transaction {array}
@param {number} [opts.txHistoryLimit] limit for how many finished
transactions can hang around in state
@param {function} opts.getNetwork return network number
@class
*/
* TransactionStatuses reimported from the shared transaction constants file
* @typedef {import('../../../../shared/constants/transaction').TransactionStatuses} TransactionStatuses
*/
/**
* TransactionStateManager is responsible for the state of a transaction and
* storing the transaction. It also has some convenience methods for finding
* subsets of transactions.
* @param {Object} opts
* @param {Object} [opts.initState={ transactions: [] }] - initial transactions list with the key transaction {Array}
* @param {number} [opts.txHistoryLimit] - limit for how many finished
* transactions can hang around in state
* @param {Function} opts.getNetwork - return network number
* @class
*/
class TransactionStateManager extends EventEmitter {
constructor ({ initState, txHistoryLimit, getNetwork }) {
super()
this.store = new ObservableStore(
Object.assign({
transactions: [],
}, initState))
this.store = new ObservableStore({ transactions: [], ...initState })
this.txHistoryLimit = txHistoryLimit
this.getNetwork = getNetwork
}
/**
@param {Object} opts - the object to use when overwriting defaults
@returns {txMeta} - the default txMeta object
*/
* @param {Object} opts - the object to use when overwriting defaults
* @returns {txMeta} the default txMeta object
*/
generateTxMeta (opts) {
const netId = this.getNetwork()
if (netId === 'loading') {
throw new Error('MetaMask is having trouble connecting to the network')
}
return Object.assign({
return {
id: createId(),
time: (new Date()).getTime(),
status: 'unapproved',
time: new Date().getTime(),
status: TRANSACTION_STATUSES.UNAPPROVED,
metamaskNetworkId: netId,
loadingDefaults: true,
}, opts)
...opts,
}
}
/**
@returns {array} - of txMetas that have been filtered for only the current network
*/
getTxList () {
* Returns the full tx list for the current network
*
* The list is iterated backwards as new transactions are pushed onto it.
*
* @param {number} [limit] - a limit for the number of transactions to return
* @returns {Object[]} The {@code txMeta}s, filtered to the current network
*/
getTxList (limit) {
const network = this.getNetwork()
const fullTxList = this.getFullTxList()
return fullTxList.filter((txMeta) => txMeta.metamaskNetworkId === network)
const nonces = new Set()
const txs = []
for (let i = fullTxList.length - 1; i > -1; i--) {
const txMeta = fullTxList[i]
if (txMeta.metamaskNetworkId !== network) {
continue
}
if (limit !== undefined) {
const { nonce } = txMeta.txParams
if (!nonces.has(nonce)) {
if (nonces.size < limit) {
nonces.add(nonce)
} else {
continue
}
}
}
txs.unshift(txMeta)
}
return txs
}
/**
@returns {array} - of all the txMetas in store
*/
* @returns {Array} of all the txMetas in store
*/
getFullTxList () {
return this.store.getState().transactions
}
/**
@returns {array} - the tx list whos status is unapproved
*/
* @returns {Array} the tx list with unapproved status
*/
getUnapprovedTxList () {
const txList = this.getTxsByMetaData('status', 'unapproved')
const txList = this.getTxsByMetaData(
'status',
TRANSACTION_STATUSES.UNAPPROVED,
)
return txList.reduce((result, tx) => {
result[tx.id] = tx
return result
@ -84,12 +112,12 @@ class TransactionStateManager extends EventEmitter {
}
/**
@param [address] {string} - hex prefixed address to sort the txMetas for [optional]
@returns {array} - the tx list whos status is approved if no address is provide
returns all txMetas who's status is approved for the current network
*/
* @param {string} [address] - hex prefixed address to sort the txMetas for [optional]
* @returns {Array} the tx list with approved status if no address is provide
* returns all txMetas with approved statuses for the current network
*/
getApprovedTransactions (address) {
const opts = { status: 'approved' }
const opts = { status: TRANSACTION_STATUSES.APPROVED }
if (address) {
opts.from = address
}
@ -97,12 +125,12 @@ class TransactionStateManager extends EventEmitter {
}
/**
@param [address] {string} - hex prefixed address to sort the txMetas for [optional]
@returns {array} - the tx list whos status is submitted if no address is provide
returns all txMetas who's status is submitted for the current network
*/
* @param {string} [address] - hex prefixed address to sort the txMetas for [optional]
* @returns {Array} the tx list submitted status if no address is provide
* returns all txMetas with submitted statuses for the current network
*/
getPendingTransactions (address) {
const opts = { status: 'submitted' }
const opts = { status: TRANSACTION_STATUSES.SUBMITTED }
if (address) {
opts.from = address
}
@ -110,12 +138,12 @@ class TransactionStateManager extends EventEmitter {
}
/**
@param [address] {string} - hex prefixed address to sort the txMetas for [optional]
@returns {array} - the tx list whos status is confirmed if no address is provide
@param {string} [address] - hex prefixed address to sort the txMetas for [optional]
@returns {Array} the tx list whose status is confirmed if no address is provide
returns all txMetas who's status is confirmed for the current network
*/
getConfirmedTransactions (address) {
const opts = { status: 'confirmed' }
const opts = { status: TRANSACTION_STATUSES.CONFIRMED }
if (address) {
opts.from = address
}
@ -123,35 +151,35 @@ class TransactionStateManager extends EventEmitter {
}
/**
Adds the txMeta to the list of transactions in the store.
if the list is over txHistoryLimit it will remove a transaction that
is in its final state
it will allso add the key `history` to the txMeta with the snap shot of the original
object
@param {Object} txMeta
@returns {Object} - the txMeta
*/
* Adds the txMeta to the list of transactions in the store.
* if the list is over txHistoryLimit it will remove a transaction that
* is in its final state.
* it will also add the key `history` to the txMeta with the snap shot of
* the original object
* @param {Object} txMeta
* @returns {Object} the txMeta
*/
addTx (txMeta) {
// normalize and validate txParams if present
if (txMeta.txParams) {
txMeta.txParams = this.normalizeAndValidateTxParams(txMeta.txParams)
}
this.once(`${txMeta.id}:signed`, function () {
this.once(`${txMeta.id}:signed`, () => {
this.removeAllListeners(`${txMeta.id}:rejected`)
})
this.once(`${txMeta.id}:rejected`, function () {
this.once(`${txMeta.id}:rejected`, () => {
this.removeAllListeners(`${txMeta.id}:signed`)
})
// initialize history
txMeta.history = []
// capture initial snapshot of txMeta for history
const snapshot = txStateHistoryHelper.snapshotFromTxMeta(txMeta)
const snapshot = snapshotFromTxMeta(txMeta)
txMeta.history.push(snapshot)
const transactions = this.getFullTxList()
const txCount = transactions.length
const txHistoryLimit = this.txHistoryLimit
const { txHistoryLimit } = this
// checks if the length of the tx history is
// longer then desired persistence limit
@ -166,8 +194,9 @@ class TransactionStateManager extends EventEmitter {
transactions.splice(index, 1)
}
}
const newTxIndex = transactions
.findIndex((currentTxMeta) => currentTxMeta.time > txMeta.time)
const newTxIndex = transactions.findIndex(
(currentTxMeta) => currentTxMeta.time > txMeta.time,
)
newTxIndex === -1
? transactions.push(txMeta)
@ -175,21 +204,22 @@ class TransactionStateManager extends EventEmitter {
this._saveTxList(transactions)
return txMeta
}
/**
@param {number} txId
@returns {Object} - the txMeta who matches the given id if none found
for the network returns undefined
*/
* @param {number} txId
* @returns {Object} the txMeta who matches the given id if none found
* for the network returns undefined
*/
getTx (txId) {
const txMeta = this.getTxsByMetaData('id', txId)[0]
return txMeta
}
/**
updates the txMeta in the list and adds a history entry
@param {Object} txMeta - the txMeta to update
@param {string} [note] - a note about the update for history
*/
* updates the txMeta in the list and adds a history entry
* @param {Object} txMeta - the txMeta to update
* @param {string} [note] - a note about the update for history
*/
updateTx (txMeta, note) {
// normalize and validate txParams if present
if (txMeta.txParams) {
@ -197,12 +227,14 @@ class TransactionStateManager extends EventEmitter {
}
// create txMeta snapshot for history
const currentState = txStateHistoryHelper.snapshotFromTxMeta(txMeta)
const currentState = snapshotFromTxMeta(txMeta)
// recover previous tx state obj
const previousState = txStateHistoryHelper.replayHistory(txMeta.history)
const previousState = replayHistory(txMeta.history)
// generate history entry and add to history
const entry = txStateHistoryHelper.generateHistoryEntry(previousState, currentState, note)
txMeta.history.push(entry)
const entry = generateHistoryEntry(previousState, currentState, note)
if (entry.length) {
txMeta.history.push(entry)
}
// commit txMeta to state
const txId = txMeta.id
@ -212,13 +244,12 @@ class TransactionStateManager extends EventEmitter {
this._saveTxList(txList)
}
/**
merges txParams obj onto txMeta.txParams
use extend to ensure that all fields are filled
@param {number} txId - the id of the txMeta
@param {Object} txParams - the updated txParams
*/
* merges txParams obj onto txMeta.txParams use extend to ensure
* that all fields are filled
* @param {number} txId - the id of the txMeta
* @param {Object} txParams - the updated txParams
*/
updateTxParams (txId, txParams) {
const txMeta = this.getTx(txId)
txMeta.txParams = { ...txMeta.txParams, ...txParams }
@ -233,15 +264,16 @@ class TransactionStateManager extends EventEmitter {
if (typeof txParams.data === 'undefined') {
delete txParams.data
}
// eslint-disable-next-line no-param-reassign
txParams = normalizeTxParams(txParams, false)
this.validateTxParams(txParams)
return txParams
}
/**
validates txParams members by type
@param {Object} txParams - txParams to validate
*/
* validates txParams members by type
* @param {Object} txParams - txParams to validate
*/
validateTxParams (txParams) {
Object.keys(txParams).forEach((key) => {
const value = txParams[key]
@ -249,12 +281,16 @@ class TransactionStateManager extends EventEmitter {
switch (key) {
case 'chainId':
if (typeof value !== 'number' && typeof value !== 'string') {
throw new Error(`${key} in txParams is not a Number or hex string. got: (${value})`)
throw new Error(
`${key} in txParams is not a Number or hex string. got: (${value})`,
)
}
break
default:
if (typeof value !== 'string') {
throw new Error(`${key} in txParams is not a string. got: (${value})`)
throw new Error(
`${key} in txParams is not a string. got: (${value})`,
)
}
break
}
@ -271,8 +307,8 @@ class TransactionStateManager extends EventEmitter {
}<br></code>
optionally the values of the keys can be functions for situations like where
you want all but one status.
@param [initialList=this.getTxList()]
@returns {array} - array of txMeta with all
@param {Array} [initialList=this.getTxList()]
@returns {Array} array of txMeta with all
options matching
*/
/*
@ -296,106 +332,104 @@ class TransactionStateManager extends EventEmitter {
})
return filteredTxList
}
/**
@param {string} key - the key to check
@param value - the value your looking for can also be a function that returns a bool
@param [txList=this.getTxList()] {array} - the list to search. default is the txList
from txStateManager#getTxList
@returns {array} - a list of txMetas who matches the search params
*/
/**
* @param {string} key - the key to check
* @param {any} value - the value your looking for can also be a function that returns a bool
* @param {Array} [txList=this.getTxList()] - the list to search. default is the txList
* from txStateManager#getTxList
* @returns {Array} a list of txMetas who matches the search params
*/
getTxsByMetaData (key, value, txList = this.getTxList()) {
const filter = typeof value === 'function' ? value : (v) => v === value
return txList.filter((txMeta) => {
if (key in txMeta.txParams) {
return filter(txMeta.txParams[key])
} else {
return filter(txMeta[key])
}
return filter(txMeta[key])
})
}
// get::set status
/**
@param {number} txId - the txMeta Id
@returns {string} - the status of the tx.
*/
* @param {number} txId - the txMeta Id
* @returns {string} the status of the tx.
*/
getTxStatus (txId) {
const txMeta = this.getTx(txId)
return txMeta.status
}
/**
should update the status of the tx to 'rejected'.
@param {number} txId - the txMeta Id
*/
* Update the status of the tx to 'rejected'.
* @param {number} txId - the txMeta Id
*/
setTxStatusRejected (txId) {
this._setTxStatus(txId, 'rejected')
this._removeTx(txId)
}
/**
should update the status of the tx to 'unapproved'.
@param {number} txId - the txMeta Id
*/
* Update the status of the tx to 'unapproved'.
* @param {number} txId - the txMeta Id
*/
setTxStatusUnapproved (txId) {
this._setTxStatus(txId, 'unapproved')
this._setTxStatus(txId, TRANSACTION_STATUSES.UNAPPROVED)
}
/**
should update the status of the tx to 'approved'.
@param {number} txId - the txMeta Id
*/
* Update the status of the tx to 'approved'.
* @param {number} txId - the txMeta Id
*/
setTxStatusApproved (txId) {
this._setTxStatus(txId, 'approved')
this._setTxStatus(txId, TRANSACTION_STATUSES.APPROVED)
}
/**
should update the status of the tx to 'signed'.
@param {number} txId - the txMeta Id
*/
* Update the status of the tx to 'signed'.
* @param {number} txId - the txMeta Id
*/
setTxStatusSigned (txId) {
this._setTxStatus(txId, 'signed')
this._setTxStatus(txId, TRANSACTION_STATUSES.SIGNED)
}
/**
should update the status of the tx to 'submitted'.
and add a time stamp for when it was called
@param {number} txId - the txMeta Id
*/
* Update the status of the tx to 'submitted' and add a time stamp
* for when it was called
* @param {number} txId - the txMeta Id
*/
setTxStatusSubmitted (txId) {
const txMeta = this.getTx(txId)
txMeta.submittedTime = (new Date()).getTime()
txMeta.submittedTime = new Date().getTime()
this.updateTx(txMeta, 'txStateManager - add submitted time stamp')
this._setTxStatus(txId, 'submitted')
this._setTxStatus(txId, TRANSACTION_STATUSES.SUBMITTED)
}
/**
should update the status of the tx to 'confirmed'.
@param {number} txId - the txMeta Id
*/
* Update the status of the tx to 'confirmed'.
* @param {number} txId - the txMeta Id
*/
setTxStatusConfirmed (txId) {
this._setTxStatus(txId, 'confirmed')
this._setTxStatus(txId, TRANSACTION_STATUSES.CONFIRMED)
}
/**
should update the status of the tx to 'dropped'.
@param {number} txId - the txMeta Id
*/
* Update the status of the tx to 'dropped'.
* @param {number} txId - the txMeta Id
*/
setTxStatusDropped (txId) {
this._setTxStatus(txId, 'dropped')
this._setTxStatus(txId, TRANSACTION_STATUSES.DROPPED)
}
/**
should update the status of the tx to 'failed'.
and put the error on the txMeta
@param {number} txId - the txMeta Id
@param {erroObject} err - error object
*/
* Updates the status of the tx to 'failed' and put the error on the txMeta
* @param {number} txId - the txMeta Id
* @param {erroObject} err - error object
*/
setTxStatusFailed (txId, err) {
const error = !err ? new Error('Internal metamask failure') : err
const error = err || new Error('Internal metamask failure')
const txMeta = this.getTx(txId)
txMeta.err = {
@ -404,47 +438,44 @@ class TransactionStateManager extends EventEmitter {
stack: error.stack,
}
this.updateTx(txMeta, 'transactions:tx-state-manager#fail - add error')
this._setTxStatus(txId, 'failed')
this._setTxStatus(txId, TRANSACTION_STATUSES.FAILED)
}
/**
Removes transaction from the given address for the current network
from the txList
@param {string} address - hex string of the from address on the txParams to remove
*/
* Removes transaction from the given address for the current network
* from the txList
* @param {string} address - hex string of the from address on the txParams
* to remove
*/
wipeTransactions (address) {
// network only tx
const txs = this.getFullTxList()
const network = this.getNetwork()
// Filter out the ones from the current account and network
const otherAccountTxs = txs.filter((txMeta) => !(txMeta.txParams.from === address && txMeta.metamaskNetworkId === network))
const otherAccountTxs = txs.filter(
(txMeta) =>
!(
txMeta.txParams.from === address &&
txMeta.metamaskNetworkId === network
),
)
// Update state
this._saveTxList(otherAccountTxs)
}
//
// PRIVATE METHODS
//
// STATUS METHODS
// statuses:
// - `'unapproved'` the user has not responded
// - `'rejected'` the user has responded no!
// - `'approved'` the user has approved the tx
// - `'signed'` the tx is signed
// - `'submitted'` the tx is sent to a server
// - `'confirmed'` the tx has been included in a block.
// - `'failed'` the tx failed for some reason, included on tx data.
// - `'dropped'` the tx nonce was already used
/**
@param {number} txId - the txMeta Id
@param {string} status - the status to set on the txMeta
@emits tx:status-update - passes txId and status
@emits ${txMeta.id}:finished - if it is a finished state. Passes the txMeta
@emits update:badge
*/
* @param {number} txId - the txMeta Id
* @param {TransactionStatuses[keyof TransactionStatuses]} status - the status to set on the txMeta
* @emits tx:status-update - passes txId and status
* @emits ${txMeta.id}:finished - if it is a finished state. Passes the txMeta
* @emits update:badge
*/
_setTxStatus (txId, status) {
const txMeta = this.getTx(txId)
@ -453,26 +484,29 @@ class TransactionStateManager extends EventEmitter {
}
txMeta.status = status
setTimeout(() => {
try {
this.updateTx(txMeta, `txStateManager: setting status to ${status}`)
this.emit(`${txMeta.id}:${status}`, txId)
this.emit(`tx:status-update`, txId, status)
if (['submitted', 'rejected', 'failed'].includes(status)) {
this.emit(`${txMeta.id}:finished`, txMeta)
}
this.emit('update:badge')
} catch (error) {
log.error(error)
try {
this.updateTx(txMeta, `txStateManager: setting status to ${status}`)
this.emit(`${txMeta.id}:${status}`, txId)
this.emit(`tx:status-update`, txId, status)
if (
[
TRANSACTION_STATUSES.SUBMITTED,
TRANSACTION_STATUSES.REJECTED,
TRANSACTION_STATUSES.FAILED,
].includes(status)
) {
this.emit(`${txMeta.id}:finished`, txMeta)
}
})
this.emit('update:badge')
} catch (error) {
log.error(error)
}
}
/**
Saves the new/updated txList.
@param {array} transactions - the list of transactions to save
*/
// Function is intended only for internal use
* Saves the new/updated txList. Intended only for internal use
* @param {Array} transactions - the list of transactions to save
*/
_saveTxList (transactions) {
this.store.updateState({ transactions })
}
@ -481,6 +515,17 @@ class TransactionStateManager extends EventEmitter {
const transactionList = this.getFullTxList()
this._saveTxList(transactionList.filter((txMeta) => txMeta.id !== txId))
}
/**
* Filters out the unapproved transactions
*/
clearUnapprovedTxs () {
const transactions = this.getFullTxList()
const nonUnapprovedTxs = transactions.filter(
(tx) => tx.status !== TRANSACTION_STATUSES.UNAPPROVED,
)
this._saveTxList(nonUnapprovedTxs)
}
}
export default TransactionStateManager

View File

@ -1,4 +1,3 @@
/**
* @typedef {Object} FirstTimeState
* @property {Object} config Initial configuration parameters
@ -10,6 +9,17 @@
*/
const initialState = {
config: {},
PreferencesController: {
frequentRpcListDetail: [
{
rpcUrl: 'http://localhost:8545',
chainId: '0x539',
ticker: 'ETH',
nickname: 'Localhost 8545',
rpcPrefs: {},
},
],
},
}
module.exports = initialState
export default initialState

View File

@ -1,5 +1,3 @@
/*global Web3*/
// need to make sure we aren't affected by overlapping namespaces
// and that we dont affect the app with our namespace
// mostly a fix for web3's BigNumber if AMD's "define" is defined...
@ -34,13 +32,11 @@ cleanContextForImports()
import log from 'loglevel'
import LocalMessageDuplexStream from 'post-message-stream'
import MetamaskInpageProvider from 'nifty-wallet-inpage-provider'
import { MetaMaskInpageProvider } from 'nifty-wallet-inpage-provider'
// TODO:deprecate:Q1-2020
import 'web3/dist/web3.min.js'
import setupDappAutoReload from './lib/auto-reload.js'
restoreContextAfterImports()
log.setDefaultLevel(process.env.METAMASK_DEBUG ? 'debug' : 'warn')
@ -56,37 +52,10 @@ const metamaskStream = new LocalMessageDuplexStream({
})
// compose the inpage provider
const inpageProvider = new MetamaskInpageProvider(metamaskStream)
// set a high max listener count to avoid unnecesary warnings
inpageProvider.setMaxListeners(100)
// Work around for web3@1.0 deleting the bound `sendAsync` but not the unbound
// `sendAsync` method on the prototype, causing `this` reference issues
const proxiedInpageProvider = new Proxy(inpageProvider, {
// straight up lie that we deleted the property so that it doesnt
// throw an error in strict mode
deleteProperty: () => true,
})
//
// TODO:deprecate:Q1-2020
//
// setup web3
const web3 = new Web3(proxiedInpageProvider)
web3.setProvider = function () {
log.debug('Nifty Wallet - overrode web3.setProvider')
}
log.debug('Nifty Wallet - injected web3')
proxiedInpageProvider._web3Ref = web3.eth
setupDappAutoReload(web3, inpageProvider.publicConfigStore)
const inpageProvider = new MetaMaskInpageProvider(metamaskStream)
//
// end deprecate:Q1-2020
//
window.ethereum = proxiedInpageProvider
window.ethereum = inpageProvider

View File

@ -1,4 +1,4 @@
import ObservableStore from 'obs-store'
import { ObservableStore } from '@metamask/obs-store'
/**
* An ObservableStore that can composes a flat

View File

@ -8,7 +8,7 @@
*/
const EthQuery = require('eth-query')
const ObservableStore = require('obs-store')
import { ObservableStore } from '@metamask/obs-store'
const log = require('loglevel')
const pify = require('pify')

View File

@ -1,76 +0,0 @@
// TODO:deprecate:Q1-2020
module.exports = setupDappAutoReload
function setupDappAutoReload (web3, observable) {
// export web3 as a global, checking for usage
let reloadInProgress = false
let lastTimeUsed
let lastSeenNetwork
let hasBeenWarned = false
global.web3 = new Proxy(web3, {
get: (_web3, key) => {
// get the time of use
lastTimeUsed = Date.now()
// show warning once on web3 access
if (!hasBeenWarned && key !== 'currentProvider') {
console.warn(`MetaMask: In Q1 2020, MetaMask will no longer inject web3. For more information, see: https://medium.com/metamask/no-longer-injecting-web3-js-4a899ad6e59e`)
hasBeenWarned = true
}
// return value normally
return _web3[key]
},
set: (_web3, key, value) => {
// set value normally
_web3[key] = value
},
})
observable.subscribe(function (state) {
// if the auto refresh on network change is false do not
// do anything
if (!window.ethereum.autoRefreshOnNetworkChange) {
return
}
// if reload in progress, no need to check reload logic
if (reloadInProgress) {
return
}
const currentNetwork = state.networkVersion
// set the initial network
if (!lastSeenNetwork) {
lastSeenNetwork = currentNetwork
return
}
// skip reload logic if web3 not used
if (!lastTimeUsed) {
return
}
// if network did not change, exit
if (currentNetwork === lastSeenNetwork) {
return
}
// initiate page reload
reloadInProgress = true
const timeSinceUse = Date.now() - lastTimeUsed
// if web3 was recently used then delay the reloading of the page
if (timeSinceUse > 500) {
triggerReset()
} else {
setTimeout(triggerReset, 500)
}
})
}
// reload the page
function triggerReset () {
global.location.reload()
}

View File

@ -1,24 +1,22 @@
/**
* Returns error without stack trace for better UI display
* @param {Error} err - error
* @returns {Error} - Error with clean stack trace.
* @returns {Error} Error with clean stack trace.
*/
function cleanErrorStack (err) {
let name = err.name
name = (name === undefined) ? 'Error' : String(name)
export default function cleanErrorStack (err) {
let { name } = err
name = name === undefined ? 'Error' : String(name)
let msg = err.message
msg = (msg === undefined) ? '' : String(msg)
msg = msg === undefined ? '' : String(msg)
if (name === '') {
err.stack = err.message
} else if (msg === '') {
err.stack = err.name
} else {
err.stack = err.name + ': ' + err.message
err.stack = `${err.name}: ${err.message}`
}
return err
}
export default cleanErrorStack

View File

@ -1,24 +1,19 @@
const WritableStream = require('readable-stream').Writable
const promiseToCallback = require('promise-to-callback')
module.exports = createStreamSink
function createStreamSink (asyncWriteFn, _opts) {
return new AsyncWritableStream(asyncWriteFn, _opts)
}
import { Writable as WritableStream } from 'readable-stream'
import promiseToCallback from 'promise-to-callback'
class AsyncWritableStream extends WritableStream {
constructor (asyncWriteFn, _opts) {
const opts = Object.assign({ objectMode: true }, _opts)
const opts = { objectMode: true, ..._opts }
super(opts)
this._asyncWriteFn = asyncWriteFn
}
// write from incomming stream to state
// write from incoming stream to state
_write (chunk, encoding, callback) {
promiseToCallback(this._asyncWriteFn(chunk, encoding))(callback)
}
}
export default function createStreamSink (asyncWriteFn, _opts) {
return new AsyncWritableStream(asyncWriteFn, _opts)
}

View File

@ -1,5 +1,5 @@
import EventEmitter from 'events'
import ObservableStore from 'obs-store'
import { ObservableStore } from '@metamask/obs-store'
import ethUtil from 'ethereumjs-util'
import { ethErrors } from 'eth-json-rpc-errors'
import log from 'loglevel'

View File

@ -1,5 +1,5 @@
import EventEmitter from 'events'
import ObservableStore from 'obs-store'
import { ObservableStore } from '@metamask/obs-store'
import { ethErrors } from 'eth-json-rpc-errors'
import log from 'loglevel'
import createId from './random-id'

View File

@ -1,28 +1,26 @@
const ethJsRpcSlug = 'Error: [ethjs-rpc] rpc error with payload '
const errorLabelPrefix = 'Error: '
export default extractEthjsErrorMessage
/**
* Extracts the important part of an ethjs-rpc error message. If the passed error is not an isEthjsRpcError, the error
* is returned unchanged.
*
* @param {string} errorMessage - The error message to parse
* @returns {string} - Returns an error message, either the same as was passed, or the ending message portion of an isEthjsRpcError
* @returns {string} Returns an error message, either the same as was passed, or the ending message portion of an isEthjsRpcError
*
* @example
* // returns 'Transaction Failed: replacement transaction underpriced'
* extractEthjsErrorMessage(`Error: [ethjs-rpc] rpc error with payload {"id":3947817945380,"jsonrpc":"2.0","params":["0xf8eb8208708477359400830398539406012c8cf97bead5deae237070f9587f8e7a266d80b8843d7d3f5a0000000000000000000000000000000000000000000000000000000000081d1a000000000000000000000000000000000000000000000000001ff973cafa800000000000000000000000000000000000000000000000000000038d7ea4c68000000000000000000000000000000000000000000000000000000000000003f48025a04c32a9b630e0d9e7ff361562d850c86b7a884908135956a7e4a336fa0300d19ca06830776423f25218e8d19b267161db526e66895567147015b1f3fc47aef9a3c7"],"method":"eth_sendRawTransaction"} Error: replacement transaction underpriced`)
*
*/
function extractEthjsErrorMessage (errorMessage) {
*/
export default function extractEthjsErrorMessage (errorMessage) {
const isEthjsRpcError = errorMessage.includes(ethJsRpcSlug)
if (isEthjsRpcError) {
const payloadAndError = errorMessage.slice(ethJsRpcSlug.length)
const originalError = payloadAndError.slice(payloadAndError.indexOf(errorLabelPrefix) + errorLabelPrefix.length)
const originalError = payloadAndError.slice(
payloadAndError.indexOf(errorLabelPrefix) + errorLabelPrefix.length,
)
return originalError
} else {
return errorMessage
}
return errorMessage
}

View File

@ -1,13 +1,19 @@
const extension = require('extensionizer')
const promisify = require('pify')
const allLocales = require('../../_locales/index.json')
import extension from 'extensionizer'
import promisify from 'pify'
import allLocales from '../../_locales/index.json'
const getPreferredLocales = extension.i18n ? promisify(
extension.i18n.getAcceptLanguages,
{ errorFirst: false },
) : async () => []
const getPreferredLocales = extension.i18n
? promisify(extension.i18n.getAcceptLanguages, { errorFirst: false })
: async () => []
const existingLocaleCodes = allLocales.map(locale => locale.code.toLowerCase().replace('_', '-'))
// mapping some browsers return hyphen instead underscore in locale codes (e.g. zh_TW -> zh-tw)
const existingLocaleCodes = {}
allLocales.forEach((locale) => {
if (locale && locale.code) {
existingLocaleCodes[locale.code.toLowerCase().replace('_', '-')] =
locale.code
}
})
/**
* Returns a preferred language code, based on settings within the user's browser. If we have no translations for the
@ -16,7 +22,7 @@ const existingLocaleCodes = allLocales.map(locale => locale.code.toLowerCase().r
* @returns {Promise<string>} Promises a locale code, either one from the user's preferred list that we have a translation for, or 'en'
*
*/
async function getFirstPreferredLangCode () {
export default async function getFirstPreferredLangCode () {
let userPreferredLocaleCodes
try {
@ -33,10 +39,10 @@ async function getFirstPreferredLangCode () {
}
const firstPreferredLangCode = userPreferredLocaleCodes
.map(code => code.toLowerCase())
.find(code => existingLocaleCodes.includes(code))
return firstPreferredLangCode || 'en'
.map((code) => code.toLowerCase().replace('_', '-'))
.find((code) =>
Object.prototype.hasOwnProperty.call(existingLocaleCodes, code),
)
return existingLocaleCodes[firstPreferredLangCode] || 'en'
}
module.exports = getFirstPreferredLangCode

View File

@ -1,6 +1,4 @@
const clone = require('clone')
module.exports = getObjStructure
import { cloneDeep } from 'lodash'
// This will create an object that represents the structure of the given object
// it replaces all values with the result of their type
@ -18,13 +16,13 @@ module.exports = getObjStructure
* Creates an object that represents the structure of the given object. It replaces all values with the result of their
* type.
*
* @param {object} obj The object for which a 'structure' will be returned. Usually a plain object and not a class.
* @returns {object} The "mapped" version of a deep clone of the passed object, with each non-object property value
* @param {Object} obj - The object for which a 'structure' will be returned. Usually a plain object and not a class.
* @returns {Object} The "mapped" version of a deep clone of the passed object, with each non-object property value
* replaced with the javascript type of that value.
*
*/
function getObjStructure (obj) {
const structure = clone(obj)
export default function getObjStructure (obj) {
const structure = cloneDeep(obj)
return deepMap(structure, (value) => {
return value === null ? 'null' : typeof value
})
@ -34,9 +32,9 @@ function getObjStructure (obj) {
* Modifies all the properties and deeply nested of a passed object. Iterates recursively over all nested objects and
* their properties, and covers the entire depth of the object. At each property value which is not an object is modified.
*
* @param {object} target The object to modify
* @param {Function} visit The modifier to apply to each non-object property value
* @returns {object} The modified object
* @param {Object} target - The object to modify
* @param {Function} visit - The modifier to apply to each non-object property value
* @returns {Object} The modified object
*/
function deepMap (target = {}, visit) {
Object.entries(target).forEach(([key, value]) => {

View File

@ -1,5 +1,5 @@
import EventEmitter from 'events'
import ObservableStore from 'obs-store'
import { ObservableStore } from '@metamask/obs-store'
import ethUtil from 'ethereumjs-util'
import { ethErrors } from 'eth-json-rpc-errors'
import createId from './random-id'

View File

@ -1,5 +1,5 @@
import EventEmitter from 'events'
import ObservableStore from 'obs-store'
import { ObservableStore } from '@metamask/obs-store'
import ethUtil from 'ethereumjs-util'
import { ethErrors } from 'eth-json-rpc-errors'
import log from 'loglevel'

View File

@ -1,16 +0,0 @@
import extractEthjsErrorMessage from './extractEthjsErrorMessage'
module.exports = reportFailedTxToSentry
//
// utility for formatting failed transaction messages
// for sending to sentry
//
function reportFailedTxToSentry ({ raven, txMeta }) {
const errorMessage = 'Transaction Failed: ' + extractEthjsErrorMessage(txMeta.err.message)
raven.captureMessage(errorMessage, {
// "extra" key is required by Sentry
extra: txMeta,
})
}

View File

@ -0,0 +1,34 @@
import handlers from './handlers'
const handlerMap = handlers.reduce((map, handler) => {
for (const methodName of handler.methodNames) {
map.set(methodName, handler.implementation)
}
return map
}, new Map())
/**
* Returns a middleware that implements the RPC methods defined in the handlers
* directory.
*
* The purpose of this middleware is to create portable RPC method
* implementations that are decoupled from the rest of our background
* architecture.
*
* Handlers consume functions that hook into the background, and only depend
* on their signatures, not e.g. controller internals.
*
* Eventually, we'll want to extract this middleware into its own package.
*
* @param {Object} opts - The middleware options
* @param {Function} opts.sendMetrics - A function for sending a metrics event
* @returns {(req: Object, res: Object, next: Function, end: Function) => void}
*/
export default function createMethodMiddleware (opts) {
return function methodMiddleware (req, res, next, end) {
if (handlerMap.has(req.method)) {
return handlerMap.get(req.method)(req, res, next, end, opts)
}
return next()
}
}

View File

@ -0,0 +1,46 @@
import { MESSAGE_TYPE } from '../../../../../shared/constants/app'
/**
* This RPC method gets background state relevant to the provider.
* The background sends RPC notifications on state changes, but the provider
* first requests state on initialization.
*/
const getProviderState = {
methodNames: [MESSAGE_TYPE.GET_PROVIDER_STATE],
implementation: getProviderStateHandler,
}
export default getProviderState
/**
* @typedef {Object} ProviderStateHandlerResult
* @property {string} chainId - The current chain ID.
* @property {boolean} isUnlocked - Whether the extension is unlocked or not.
* @property {string} networkVersion - The current network ID.
*/
/**
* @typedef {Object} ProviderStateHandlerOptions
* @property {() => ProviderStateHandlerResult} getProviderState - A function that
* gets the current provider state.
*/
/**
* @param {import('json-rpc-engine').JsonRpcRequest<[]>} req - The JSON-RPC request object.
* @param {import('json-rpc-engine').JsonRpcResponse<ProviderStateHandlerResult>} res - The JSON-RPC response object.
* @param {Function} _next - The json-rpc-engine 'next' callback.
* @param {Function} end - The json-rpc-engine 'end' callback.
* @param {ProviderStateHandlerOptions} options
*/
async function getProviderStateHandler (
req,
res,
_next,
end,
{ getProviderState: _getProviderState },
) {
res.result = {
...(await _getProviderState(req.origin)),
}
return end()
}

View File

@ -0,0 +1,6 @@
import getProviderState from './get-provider-state'
import logWeb3ShimUsage from './log-web3-shim-usage'
import watchAsset from './watch-asset'
const handlers = [getProviderState, logWeb3ShimUsage, watchAsset]
export default handlers

View File

@ -0,0 +1,57 @@
import { MESSAGE_TYPE } from '../../../../../shared/constants/app'
/**
* This RPC method is called by the inpage provider whenever it detects the
* accessing of a non-existent property on our window.web3 shim.
* We collect this data to understand which sites are breaking due to the
* removal of our window.web3.
*/
const logWeb3ShimUsage = {
methodNames: [MESSAGE_TYPE.LOG_WEB3_SHIM_USAGE],
implementation: logWeb3ShimUsageHandler,
}
export default logWeb3ShimUsage
/**
* @typedef {Object} LogWeb3ShimUsageOptions
* @property {Function} sendMetrics - A function that registers a metrics event.
* @property {Function} getWeb3ShimUsageState - A function that gets web3 shim
* usage state for the given origin.
* @property {Function} setWeb3ShimUsageRecorded - A function that records web3 shim
* usage for a particular origin.
*/
/**
* @param {import('json-rpc-engine').JsonRpcRequest<unknown>} req - The JSON-RPC request object.
* @param {import('json-rpc-engine').JsonRpcResponse<true>} res - The JSON-RPC response object.
* @param {Function} _next - The json-rpc-engine 'next' callback.
* @param {Function} end - The json-rpc-engine 'end' callback.
* @param {LogWeb3ShimUsageOptions} options
*/
function logWeb3ShimUsageHandler (
req,
res,
_next,
end,
{ sendMetrics, getWeb3ShimUsageState, setWeb3ShimUsageRecorded },
) {
const { origin } = req
if (getWeb3ShimUsageState(origin) === undefined) {
setWeb3ShimUsageRecorded(origin)
sendMetrics({
event: `Website Accessed window.web3 Shim`,
category: 'inpage_provider',
eventContext: {
referrer: {
url: origin,
},
},
excludeMetaMetricsId: true,
})
}
res.result = true
return end()
}

View File

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

View File

@ -0,0 +1 @@
export { default } from './createMethodMiddleware'

View File

@ -1,6 +1,6 @@
import EventEmitter from 'events'
import assert from 'assert'
import ObservableStore from 'obs-store'
import { ObservableStore } from '@metamask/obs-store'
import { ethErrors } from 'eth-json-rpc-errors'
import { typedSignatureHash, TYPED_MESSAGE_SCHEMA } from 'eth-sig-util'
import { isValidAddress } from 'ethereumjs-util'

View File

@ -1,36 +1,30 @@
/**
* @file The central metamask controller. Aggregates other controllers and exports an api.
* @copyright Copyright (c) 2018 MetaMask
* @license MIT
*/
import EventEmitter from 'events'
import pump from 'pump'
import Dnode from 'dnode'
import extension from 'extensionizer'
import ObservableStore from 'obs-store'
import { ObservableStore } from '@metamask/obs-store'
const ComposableObservableStore = require('./lib/ComposableObservableStore')
import asStream from 'obs-store/lib/asStream'
const AccountTracker = require('./lib/account-tracker')
import RpcEngine from 'json-rpc-engine'
import { storeAsStream } from '@metamask/obs-store/dist/asStream'
import { JsonRpcEngine } from 'json-rpc-engine'
import { debounce } from 'lodash'
const createEngineStream = require('json-rpc-middleware-stream/engineStream')
const createFilterMiddleware = require('eth-json-rpc-filters')
const createSubscriptionManager = require('eth-json-rpc-filters/subscriptionManager')
import createEngineStream from 'json-rpc-middleware-stream/engineStream'
import createFilterMiddleware from 'eth-json-rpc-filters'
import createSubscriptionManager from 'eth-json-rpc-filters/subscriptionManager'
import providerAsMiddleware from 'eth-json-rpc-middleware/providerAsMiddleware'
const createOriginMiddleware = require('./lib/createOriginMiddleware')
import createLoggerMiddleware from './lib/createLoggerMiddleware'
import createTabIdMiddleware from './lib/createTabIdMiddleware'
import providerAsMiddleware from 'eth-json-rpc-middleware/providerAsMiddleware'
const setupMultiplex = require('./lib/stream-utils.js').setupMultiplex
import { setupMultiplex } from './lib/stream-utils'
const KeyringController = require('eth-keychain-controller')
import { Mutex } from 'await-semaphore'
const AccountTracker = require('./lib/account-tracker')
import createMethodMiddleware from './lib/rpc-method-middleware'
import { TRANSACTION_STATUSES } from '../../shared/constants/transaction'
const NetworkController = require('./controllers/network')
const PreferencesController = require('./controllers/preferences')
const CurrencyController = require('./controllers/currency')
const NoticeController = require('./notice-controller')
const ShapeShiftController = require('./controllers/shapeshift')
const AddressBookController = require('./controllers/address-book')
const InfuraController = require('./controllers/infura')
const CachedBalancesController = require('./controllers/cached-balances')
const RecentBlocksController = require('./controllers/recent-blocks')
import MessageManager from './lib/message-manager'
@ -43,11 +37,10 @@ const BalancesController = require('./controllers/computed-balances')
const TokenRatesController = require('./controllers/token-rates')
const DetectTokensController = require('./controllers/detect-tokens')
import { PermissionsController } from './controllers/permissions'
import { NOTIFICATION_NAMES } from './controllers/permissions/enums'
import getRestrictedMethods from './controllers/permissions/restrictedMethods'
const nodeify = require('./lib/nodeify')
const accountImporter = require('./account-import-strategies')
import { Mutex } from 'await-semaphore'
import selectChainId from './lib/select-chain-id'
const version = require('../manifest.json').version
import ethUtil, { BN } from 'ethereumjs-util'
const GWEI_BN = new BN('1000000000')
@ -86,6 +79,8 @@ module.exports = class MetamaskController extends EventEmitter {
this.sendUpdate = debounce(this.privateSendUpdate.bind(this), 200)
this.opts = opts
this.extension = opts.extension
this.platform = opts.platform
const initState = opts.initState || {}
this.recordFirstTimeInfo(initState)
@ -93,8 +88,6 @@ module.exports = class MetamaskController extends EventEmitter {
// the only thing that uses controller connections are open metamask UI instances
this.activeControllerConnections = 0
// platform-specific api
this.platform = opts.platform
// observable state store
this.store = new ComposableObservableStore(initState)
@ -109,6 +102,7 @@ module.exports = class MetamaskController extends EventEmitter {
// network store
this.networkController = new NetworkController(initState.NetworkController)
this.networkController.setEthMainnetRPCEndpoint(opts.ethMainnetRpcEndpoint)
this.networkController.setInfuraProjectId(opts.infuraProjectId)
// preferences controller
this.preferencesController = new PreferencesController({
@ -125,12 +119,6 @@ module.exports = class MetamaskController extends EventEmitter {
this.currencyController.updateConversionRate()
this.currencyController.scheduleConversionInterval()
// infura controller
this.infuraController = new InfuraController({
initState: initState.InfuraController,
})
this.infuraController.scheduleInfuraNetworkCheck()
this.phishingController = new PhishingController()
// rpc provider
@ -241,6 +229,9 @@ module.exports = class MetamaskController extends EventEmitter {
this.txController = new TransactionController({
initState: initState.TransactionController || initState.TransactionManager,
networkStore: this.networkController.networkStore,
getCurrentChainId: this.networkController.getCurrentChainId.bind(
this.networkController,
),
preferencesStore: this.preferencesController.store,
txHistoryLimit: 40,
getNetwork: this.networkController.getNetworkState.bind(this),
@ -251,8 +242,11 @@ module.exports = class MetamaskController extends EventEmitter {
})
this.txController.on('newUnapprovedTx', () => opts.showUnapprovedTx())
this.txController.on(`tx:status-update`, (txId, status) => {
if (status === 'confirmed' || status === 'failed') {
this.txController.on(`tx:status-update`, async (txId, status) => {
if (
status === TRANSACTION_STATUSES.CONFIRMED ||
status === TRANSACTION_STATUSES.FAILED
) {
const txMeta = this.txController.txStateManager.getTx(txId)
this.platform.showTransactionNotification(txMeta)
}
@ -292,9 +286,7 @@ module.exports = class MetamaskController extends EventEmitter {
})
// ensure isClientOpenAndUnlocked is updated when memState updates
this.on('update', (memState) => {
this.isClientOpenAndUnlocked = memState.isUnlocked && this._isClientOpen
})
this.on('update', (memState) => this._onStateUpdate(memState))
this.store.updateStructure({
TransactionController: this.txController.store,
@ -305,7 +297,6 @@ module.exports = class MetamaskController extends EventEmitter {
NoticeController: this.noticeController.store,
ShapeShiftController: this.shapeshiftController.store,
NetworkController: this.networkController.store,
InfuraController: this.infuraController.store,
CachedBalancesController: this.cachedBalancesController.store,
PermissionsController: this.permissionsController.permissions,
PermissionsMetadata: this.permissionsController.store,
@ -330,11 +321,13 @@ module.exports = class MetamaskController extends EventEmitter {
CurrencyController: this.currencyController.store,
NoticeController: this.noticeController.memStore,
ShapeshiftController: this.shapeshiftController.store,
InfuraController: this.infuraController.store,
PermissionsController: this.permissionsController.permissions,
PermissionsMetadata: this.permissionsController.store,
})
this.memStore.subscribe(this.sendUpdate.bind(this))
// TODO:LegacyProvider: Delete
this.publicConfigStore = this.createPublicConfigStore()
}
/**
@ -367,7 +360,11 @@ module.exports = class MetamaskController extends EventEmitter {
processDecryptMessage: this.newRequestDecryptMessage.bind(this),
processEncryptionPublicKey: this.newRequestEncryptionPublicKey.bind(this),
getPendingNonce: this.getPendingNonce.bind(this),
getPendingTransactionByHash: (hash) => this.txController.getFilteredTxList({ hash, status: 'submitted' })[0],
getPendingTransactionByHash: (hash) =>
this.txController.getFilteredTxList({
hash,
status: TRANSACTION_STATUSES.SUBMITTED,
})[0],
}
const providerProxy = this.networkController.initializeProvider(providerOpts)
return providerProxy
@ -380,30 +377,64 @@ module.exports = class MetamaskController extends EventEmitter {
createPublicConfigStore () {
// subset of state for metamask inpage provider
const publicConfigStore = new ObservableStore()
const { networkController } = this
// setup memStore subscription hooks
this.on('update', updatePublicConfigStore)
updatePublicConfigStore(this.getState())
publicConfigStore.destroy = () => {
this.removeEventListener && this.removeEventListener('update', updatePublicConfigStore)
}
function updatePublicConfigStore (memState) {
publicConfigStore.putState(selectPublicState(memState))
const chainId = networkController.getCurrentChainId()
if (memState.network !== 'loading') {
publicConfigStore.putState(selectPublicState(chainId, memState))
}
}
function selectPublicState ({ isUnlocked, network, provider, selectedAddress }) {
function selectPublicState (chainId, { isUnlocked, network, selectedAddress }) {
return {
isUnlocked,
chainId,
selectedAddress: isUnlocked ? selectedAddress : undefined,
networkVersion: network,
chainId: selectChainId({ network, provider }),
}
}
return publicConfigStore
}
/**
* Gets relevant state for the provider of an external origin.
*
* @param {string} origin - The origin to get the provider state for.
* @returns {Promise<{
* isUnlocked: boolean,
* networkVersion: string,
* chainId: string,
* accounts: string[],
* }>} An object with relevant state properties.
*/
async getProviderState (origin) {
return {
isUnlocked: this.isUnlocked(),
...this.getProviderNetworkState(),
accounts: await this.permissionsController.getAccounts(origin),
}
}
/**
* Gets network state relevant for external providers.
*
* @param {Object} [memState] - The MetaMask memState. If not provided,
* this function will retrieve the most recent state.
* @returns {Object} An object with relevant network state properties.
*/
getProviderNetworkState (memState) {
const { network } = memState || this.getState()
return {
chainId: this.networkController.getCurrentChainId(),
networkVersion: network,
}
}
//=============================================================================
// EXPOSED TO THE UI SUBSYSTEM
//=============================================================================
@ -1536,8 +1567,8 @@ module.exports = class MetamaskController extends EventEmitter {
*/
setupUntrustedCommunication (connectionStream, sender) {
const { usePhishDetect } = this.preferencesController.store.getState()
const hostname = (new URL(sender.url)).hostname
// Check if new connection is blacklisted if phishing detection is on
const { hostname } = new URL(sender.url)
// Check if new connection is blocked if phishing detection is on
if (usePhishDetect && this.phishingController.test(hostname)) {
log.debug('Nifty Wallet - sending phishing warning for', hostname)
this.sendPhishingWarning(connectionStream, hostname)
@ -1548,7 +1579,7 @@ module.exports = class MetamaskController extends EventEmitter {
const mux = setupMultiplex(connectionStream)
// messages between inpage and background
this.setupProviderConnection(mux.createStream('provider'), sender)
this.setupProviderConnection(mux.createStream('metamask-provider'), sender)
this.setupPublicConfig(mux.createStream('publicConfig'))
}
@ -1625,11 +1656,9 @@ module.exports = class MetamaskController extends EventEmitter {
* @param {boolean} isInternal - True if this is a connection with an internal process
*/
setupProviderConnection (outStream, sender, isInternal) {
const origin = isInternal
? 'metamask'
: (new URL(sender.url)).hostname
const origin = isInternal ? 'metamask' : new URL(sender.url).origin
let extensionId
if (sender.id !== extension.runtime.id) {
if (sender.id !== this.extension.runtime.id) {
extensionId = sender.id
}
let tabId
@ -1637,30 +1666,31 @@ module.exports = class MetamaskController extends EventEmitter {
tabId = sender.tab.id
}
const engine = this.setupProviderEngine({ origin, location: sender.url, extensionId, tabId })
const engine = this.setupProviderEngine({
origin,
location: sender.url,
extensionId,
tabId,
isInternal,
})
// setup connection
const providerStream = createEngineStream({ engine })
const connectionId = this.addConnection(origin, { engine })
pump(
outStream,
providerStream,
outStream,
(err) => {
// handle any middleware cleanup
engine._middleware.forEach((mid) => {
if (mid.destroy && typeof mid.destroy === 'function') {
mid.destroy()
}
})
connectionId && this.removeConnection(origin, connectionId)
if (err) {
log.error(err)
pump(outStream, providerStream, outStream, (err) => {
// handle any middleware cleanup
engine._middleware.forEach((mid) => {
if (mid.destroy && typeof mid.destroy === 'function') {
mid.destroy()
}
},
)
})
connectionId && this.removeConnection(origin, connectionId)
if (err) {
log.error(err)
}
})
}
/**
@ -1673,7 +1703,7 @@ module.exports = class MetamaskController extends EventEmitter {
**/
setupProviderEngine ({ origin, location, extensionId, tabId }) {
// setup json rpc engine stack
const engine = new RpcEngine()
const engine = new JsonRpcEngine()
const provider = this.provider
const blockTracker = this.blockTracker
@ -1692,6 +1722,15 @@ module.exports = class MetamaskController extends EventEmitter {
}
// logging
engine.push(createLoggerMiddleware({ origin }))
engine.push(
createMethodMiddleware({
origin,
getProviderState: this.getProviderState.bind(this),
handleWatchAssetRequest: this.preferencesController.requestWatchAsset.bind(
this.preferencesController,
),
}),
)
// filter and subscription polyfills
engine.push(filterMiddleware)
engine.push(subscriptionManager.middleware)
@ -1705,6 +1744,7 @@ module.exports = class MetamaskController extends EventEmitter {
}
/**
* TODO:LegacyProvider: Delete
* A method for providing our public config info over a stream.
* This includes info we like to be synchronous if possible, like
* the current selected account, and network ID.
@ -1715,20 +1755,14 @@ module.exports = class MetamaskController extends EventEmitter {
* @param {*} outStream - The stream to provide public config over.
*/
setupPublicConfig (outStream) {
const configStore = this.createPublicConfigStore()
const configStream = asStream(configStore)
const configStream = storeAsStream(this.publicConfigStore)
pump(
configStream,
outStream,
(err) => {
configStore.destroy()
configStream.destroy()
if (err) {
log.error(err)
}
},
)
pump(configStream, outStream, (err) => {
configStream.destroy()
if (err) {
log.error(err)
}
})
}
/**
@ -1739,10 +1773,9 @@ module.exports = class MetamaskController extends EventEmitter {
* @param {string} origin - The connection's origin string.
* @param {Object} options - Data associated with the connection
* @param {Object} options.engine - The connection's JSON Rpc Engine
* @returns {string} - The connection's id (so that it can be deleted later)
* @returns {string} The connection's id (so that it can be deleted later)
*/
addConnection (origin, { engine }) {
if (origin === 'metamask') {
return null
}
@ -1767,7 +1800,6 @@ module.exports = class MetamaskController extends EventEmitter {
* @param {string} id - The connection's id, as returned from addConnection.
*/
removeConnection (origin, id) {
const connections = this.connections[origin]
if (!connections) {
return
@ -1775,7 +1807,7 @@ module.exports = class MetamaskController extends EventEmitter {
delete connections[id]
if (Object.keys(connections.length === 0)) {
if (Object.keys(connections).length === 0) {
delete this.connections[origin]
}
}
@ -1783,49 +1815,59 @@ module.exports = class MetamaskController extends EventEmitter {
/**
* Causes the RPC engines associated with the connections to the given origin
* to emit a notification event with the given payload.
* Does nothing if the extension is locked or the origin is unknown.
*
* The caller is responsible for ensuring that only permitted notifications
* are sent.
*
* Ignores unknown origins.
*
* @param {string} origin - The connection's origin string.
* @param {any} payload - The event payload.
*/
notifyConnections (origin, payload) {
const { isUnlocked } = this.getState()
const connections = this.connections[origin]
if (!isUnlocked || !connections) {
return
}
Object.values(connections).forEach((conn) => {
conn.engine && conn.engine.emit('notification', payload)
})
if (connections) {
Object.values(connections).forEach((conn) => {
if (conn.engine) {
conn.engine.emit('notification', payload)
}
})
}
}
/**
* Causes the RPC engines associated with all connections to emit a
* notification event with the given payload.
* Does nothing if the extension is locked.
*
* @param {any} payload - The event payload.
* If the "payload" parameter is a function, the payload for each connection
* will be the return value of that function called with the connection's
* origin.
*
* The caller is responsible for ensuring that only permitted notifications
* are sent.
*
* @param {any} payload - The event payload, or payload getter function.
*/
notifyAllConnections (payload) {
const { isUnlocked } = this.getState()
if (!isUnlocked) {
return
}
const getPayload =
typeof payload === 'function'
? (origin) => payload(origin)
: () => payload
Object.values(this.connections).forEach((origin) => {
Object.values(origin).forEach((conn) => {
conn.engine && conn.engine.emit('notification', payload)
if (conn.engine) {
conn.engine.emit('notification', getPayload(origin))
}
})
})
}
/**
* Handle a KeyringController update
* @param {object} state the KC state
* @return {Promise<void>}
* @param {Object} state - the KC state
* @returns {Promise<void>}
* @private
*/
async _onKeyringControllerUpdate (state) {
@ -1850,6 +1892,53 @@ module.exports = class MetamaskController extends EventEmitter {
}
}
/**
* Handle global unlock, triggered by KeyringController unlock.
* Notifies all connections that the extension is unlocked.
*/
_onUnlock () {
this.notifyAllConnections((origin) => {
return {
method: NOTIFICATION_NAMES.unlockStateChanged,
params: {
isUnlocked: true,
accounts: this.permissionsController.getAccounts(origin),
},
}
})
this.emit('unlock')
}
/**
* Handle global lock, triggered by KeyringController lock.
* Notifies all connections that the extension is locked.
*/
_onLock () {
this.notifyAllConnections({
method: NOTIFICATION_NAMES.unlockStateChanged,
params: {
isUnlocked: false,
},
})
this.emit('lock')
}
/**
* Handle memory state updates.
* - Ensure isClientOpenAndUnlocked is updated
* - Notifies all connections with the new provider network state
* - The external providers handle diffing the state
*/
_onStateUpdate (newState) {
this.isClientOpenAndUnlocked = newState.isUnlocked && this._isClientOpen
this.notifyAllConnections({
method: NOTIFICATION_NAMES.chainChanged,
params: this.getProviderNetworkState(newState),
})
}
// misc
/**
* A method for emitting the full MetaMask state to all registered listeners.
* @private

View File

@ -7,7 +7,7 @@ This migration updates "transaction state history" to diffs style
*/
const clone = require('clone')
import txStateHistoryHelper from '../controllers/transactions/lib/tx-state-history-helper'
import { migrateFromSnapshotsToDiffs, snapshotFromTxMeta } from '../controllers/transactions/lib/tx-state-history-helpers'
module.exports = {
@ -35,13 +35,13 @@ function transformState (state) {
newState.TransactionController.transactions = transactions.map((txMeta) => {
// no history: initialize
if (!txMeta.history || txMeta.history.length === 0) {
const snapshot = txStateHistoryHelper.snapshotFromTxMeta(txMeta)
const snapshot = snapshotFromTxMeta(txMeta)
txMeta.history = [snapshot]
return txMeta
}
// has history: migrate
const newHistory = (
txStateHistoryHelper.migrateFromSnapshotsToDiffs(txMeta.history)
migrateFromSnapshotsToDiffs(txMeta.history)
// remove empty diffs
.filter((entry) => {
return !Array.isArray(entry) || entry.length > 0

View File

@ -1,7 +1,7 @@
const EventEmitter = require('events').EventEmitter
const semver = require('semver')
const extend = require('xtend')
const ObservableStore = require('obs-store')
import { ObservableStore } from '@metamask/obs-store'
const hardCodedNotices = require('../../notices/notices.js')
const uniqBy = require('lodash.uniqby')

View File

@ -8,11 +8,11 @@ window.onload = function () {
const querystring = require('querystring')
const dnode = require('dnode')
const { EventEmitter } = require('events')
const PortStream = require('extension-port-stream')
import PortStream from 'extension-port-stream'
const extension = require('extensionizer')
const setupMultiplex = require('./lib/stream-utils.js').setupMultiplex
const { getEnvironmentType } = require('./lib/util')
const ExtensionPlatform = require('./platforms/extension')
import ExtensionPlatform from './platforms/extension'
document.addEventListener('DOMContentLoaded', start)

View File

@ -1,10 +1,10 @@
import extension from 'extensionizer'
const explorerLinks = require('eth-net-props').explorerLinks
const { capitalizeFirstLetter, getEnvironmentType, checkForError } = require('../lib/util')
const { ENVIRONMENT_TYPE_BACKGROUND } = require('../lib/enums')
class ExtensionPlatform {
import { getEnvironmentType, checkForError } from '../lib/util'
import { ENVIRONMENT_TYPE_BACKGROUND } from '../lib/enums'
import { TRANSACTION_STATUSES } from '../../../shared/constants/transaction'
export default class ExtensionPlatform {
//
// Public
//
@ -36,9 +36,9 @@ class ExtensionPlatform {
})
}
closeWindow (windowId) {
focusWindow (windowId) {
return new Promise((resolve, reject) => {
extension.windows.remove(windowId, () => {
extension.windows.update(windowId, { focused: true }, () => {
const error = checkForError()
if (error) {
return reject(error)
@ -48,9 +48,9 @@ class ExtensionPlatform {
})
}
focusWindow (windowId) {
updateWindowPosition (windowId, left, top) {
return new Promise((resolve, reject) => {
extension.windows.update(windowId, { focused: true }, () => {
extension.windows.update(windowId, { left, top }, () => {
const error = checkForError()
if (error) {
return reject(error)
@ -105,18 +105,22 @@ class ExtensionPlatform {
})
} catch (e) {
cb(e)
return
}
}
showTransactionNotification (txMeta) {
const { status, txReceipt: { status: receiptStatus } = {} } = txMeta
if (status === 'confirmed') {
if (status === TRANSACTION_STATUSES.CONFIRMED) {
// There was an on-chain failure
receiptStatus === '0x0'
? this._showFailedTransaction(txMeta, 'Transaction encountered an error.')
? this._showFailedTransaction(
txMeta,
'Transaction encountered an error.',
)
: this._showConfirmedTransaction(txMeta)
} else if (status === 'failed') {
} else if (status === TRANSACTION_STATUSES.FAILED) {
this._showFailedTransaction(txMeta)
}
}
@ -185,7 +189,6 @@ class ExtensionPlatform {
}
_showConfirmedTransaction (txMeta) {
this._subscribeToNotificationClicked()
const { url, explorerName } = this._getExplorer(txMeta.hash, parseInt(txMeta.metamaskNetworkId))
@ -197,29 +200,27 @@ class ExtensionPlatform {
}
_showFailedTransaction (txMeta, errorMessage) {
const nonce = parseInt(txMeta.txParams.nonce, 16)
const title = 'Failed transaction'
const message = `Transaction ${nonce} failed! ${errorMessage || capitalizeFirstLetter(txMeta.err.message)}`
const message = `Transaction ${nonce} failed! ${
errorMessage || txMeta.err.message
}`
this._showNotification(title, message)
}
_showNotification (title, message, url) {
extension.notifications.create(
url,
{
'type': 'basic',
'title': title,
'iconUrl': extension.extension.getURL('../../images/icon-64.png'),
'message': message,
})
extension.notifications.create(url, {
type: 'basic',
title,
iconUrl: extension.extension.getURL('../../images/icon-64.png'),
message,
})
}
_subscribeToNotificationClicked () {
if (!extension.notifications.onClicked.hasListener(this._viewOnExplorer)) {
extension.notifications.onClicked.removeListener(this._viewOnExplorer)
extension.notifications.onClicked.addListener(this._viewOnExplorer)
}
extension.notifications.onClicked.addListener(this._viewOnExplorer)
}
_viewOnExplorer (url) {
@ -237,5 +238,3 @@ class ExtensionPlatform {
}
}
}
module.exports = ExtensionPlatform

View File

@ -1,10 +1,10 @@
const injectCss = require('inject-css')
const OldMetaMaskUiCss = require('../../old-ui/css')
const startPopup = require('./popup-core')
const PortStream = require('extension-port-stream')
import PortStream from 'extension-port-stream'
const { getEnvironmentType } = require('./lib/util')
import extension from 'extensionizer'
const ExtensionPlatform = require('./platforms/extension')
import ExtensionPlatform from './platforms/extension'
const setupRaven = require('./lib/setupRaven')
const log = require('loglevel')
@ -37,7 +37,7 @@ async function start () {
if (err) return displayCriticalError(err)
// Code commented out until we begin auto adding users to NewUI
// const { isMascara, identities = {}, featureFlags = {} } = store.getState().metamask
// const { identities = {}, featureFlags = {} } = store.getState().metamask
// const firstTime = Object.keys(identities).length === 0
// Code commented out until we begin auto adding users to NewUI

View File

@ -24,7 +24,6 @@ async function start () {
const SHORT_SHA1 = CIRCLE_SHA1.slice(0, 7)
const BUILD_LINK_BASE = `https://${CIRCLE_BUILD_NUM}-42009758-gh.circle-artifacts.com/0`
const MASCARA = `${BUILD_LINK_BASE}/builds/mascara/home.html`
const CHROME = `${BUILD_LINK_BASE}/builds/niftywallet-chrome-${VERSION}.zip`
const FIREFOX = `${BUILD_LINK_BASE}/builds/niftywallet-firefox-${VERSION}.zip`
const EDGE = `${BUILD_LINK_BASE}/builds/niftywallet-edge-${VERSION}.zip`
@ -35,7 +34,6 @@ async function start () {
<details>
<summary>
Builds ready [${SHORT_SHA1}]:
<a href="${MASCARA}">mascara</a>,
<a href="${CHROME}">chrome</a>,
<a href="${FIREFOX}">firefox</a>,
<a href="${EDGE}">edge</a>,

View File

@ -22,7 +22,7 @@ const backGroundConnectionModifiers = require('./backGroundConnectionModifiers')
const Selector = require('./selector')
const MetamaskController = require('../app/scripts/metamask-controller')
const firstTimeState = require('../app/scripts/first-time-state')
const ExtensionPlatform = require('../app/scripts/platforms/extension')
import ExtensionPlatform from '../app/scripts/platforms/extension'
const noop = function () {}
const log = require('loglevel')

View File

@ -2,7 +2,6 @@
"metamask": {
"isInitialized": true,
"isUnlocked": true,
"isMascara": false,
"rpcTarget": "https://rawtestrpc.metamask.io/",
"identities": {
"0xfdea65c8e26263f6d9a1b5de9555d2931a33b825": {

View File

@ -31,6 +31,7 @@ function gulpParallel (...args) {
}
const conf = require('rc')('niftywallet', {
INFURA_PROJECT_ID: process.env.INFURA_PROJECT_ID,
ETH_MAINNET_RPC_ENDPOINT: process.env.ETH_MAINNET_RPC_ENDPOINT,
})
@ -41,8 +42,6 @@ const browserPlatforms = [
'opera',
]
const commonPlatforms = [
// browser webapp
'mascara',
// browser extensions
...browserPlatforms,
]
@ -69,7 +68,7 @@ createCopyTasks('images', {
destinations: commonPlatforms.map(platform => `./dist/${platform}/images`),
})
createCopyTasks('contractImages', {
source: './node_modules/eth-contract-metadata/images/',
source: './node_modules/@metamask/contract-metadata/images/',
destinations: commonPlatforms.map(platform => `./dist/${platform}/images/contract`),
})
createCopyTasks('contractImagesPOA', {
@ -112,14 +111,6 @@ createCopyTasks('manifest', {
destinations: browserPlatforms.map(platform => `./dist/${platform}`),
})
// copy mascara
createCopyTasks('html:mascara', {
source: './mascara/',
pattern: 'proxy/index.html',
destinations: [`./dist/mascara/`],
})
function createCopyTasks (label, opts) {
if (!opts.devOnly) {
const copyTaskName = `copy:${label}`
@ -254,8 +245,6 @@ const buildJsFiles = [
// bundle tasks
createTasksForBuildJsExtension({ buildJsFiles, taskPrefix: 'dev:extension:js', devMode: true })
createTasksForBuildJsExtension({ buildJsFiles, taskPrefix: 'build:extension:js' })
createTasksForBuildJsMascara({ taskPrefix: 'build:mascara:js' })
createTasksForBuildJsMascara({ taskPrefix: 'dev:mascara:js', devMode: true })
function createTasksForBuildJsExtension ({ buildJsFiles, taskPrefix, devMode, bundleTaskOpts = {} }) {
// inpage must be built before all other scripts:
@ -275,22 +264,6 @@ function createTasksForBuildJsExtension ({ buildJsFiles, taskPrefix, devMode, bu
createTasksForBuildJs({ rootDir, taskPrefix, bundleTaskOpts, destinations, buildPhase1, buildPhase2 })
}
function createTasksForBuildJsMascara ({ taskPrefix, devMode, bundleTaskOpts = {} }) {
// inpage must be built before all other scripts:
const rootDir = './mascara/src/'
const buildPhase1 = ['ui', 'proxy', 'background', 'metamascara']
const destinations = ['./dist/mascara']
bundleTaskOpts = Object.assign({
buildSourceMaps: true,
sourceMapDir: './',
minifyBuild: false,
buildWithFullPaths: devMode,
watch: devMode,
devMode,
}, bundleTaskOpts)
createTasksForBuildJs({ rootDir, taskPrefix, bundleTaskOpts, destinations, buildPhase1 })
}
function createTasksForBuildJs ({ rootDir, taskPrefix, bundleTaskOpts, destinations, buildPhase1 = [], buildPhase2 = [] }) {
// bundle task for each file
const jsFiles = [].concat(buildPhase1, buildPhase2)
@ -338,7 +311,6 @@ gulp.task('dev',
'clean',
gulp.parallel(
'dev:extension:js',
'dev:mascara:js',
'dev:copy',
'dev:reload',
),
@ -356,23 +328,11 @@ gulp.task('dev:extension',
),
)
gulp.task('dev:mascara',
gulp.series(
'clean',
gulp.parallel(
'dev:mascara:js',
'dev:copy',
'dev:reload',
),
),
)
gulp.task('build',
gulp.series(
'clean',
gulpParallel(
'build:extension:js',
'build:mascara:js',
'copy',
),
),
@ -388,16 +348,6 @@ gulp.task('build:extension',
),
)
gulp.task('build:mascara',
gulp.series(
'clean',
gulp.parallel(
'build:mascara:js',
'copy',
),
),
)
gulp.task('dist',
gulp.series(
'build',
@ -430,6 +380,7 @@ function generateBundler (opts, performBundle) {
METAMASK_DEBUG: opts.devMode,
NODE_ENV: opts.devMode ? 'development' : 'production',
ETH_MAINNET_RPC_ENDPOINT: conf.ETH_MAINNET_RPC_ENDPOINT,
INFURA_PROJECT_ID: conf.INFURA_PROJECT_ID,
}))
if (opts.watch) {
@ -510,7 +461,7 @@ function bundleTask (opts) {
buildStream = buildStream
.pipe(uglify({
mangle: {
reserved: [ 'MetamaskInpageProvider' ],
reserved: [ 'MetaMaskInpageProvider' ],
},
}))
}

View File

@ -1,33 +0,0 @@
start the dual servers (dapp + mascara)
```
npm run mascara
```
### First time use:
- navigate to: http://localhost:9001
- Create an Account
- go back to http://localhost:9002
- open devTools
- click Sync Tx
### Tests:
```
npm run testMascara
```
Test will run in browser, you will have to have these browsers installed:
- Chrome
- Firefox
- Opera
### Deploy:
Will build and deploy mascara via docker
```
docker-compose build && docker-compose stop && docker-compose up -d && docker-compose logs --tail 200 -f
```

View File

@ -1,38 +0,0 @@
const EthQuery = require('ethjs-query')
window.addEventListener('load', loadProvider)
window.addEventListener('message', console.warn)
async function loadProvider () {
const ethereumProvider = window.metamask.createDefaultProvider({ host: 'http://localhost:9001' })
const ethQuery = new EthQuery(ethereumProvider)
const accounts = await ethQuery.accounts()
window.METAMASK_ACCOUNT = accounts[0] || 'locked'
logToDom(accounts.length ? accounts[0] : 'LOCKED or undefined', 'account')
setupButtons(ethQuery)
}
function logToDom (message, context) {
document.getElementById(context).innerText = message
console.log(message)
}
function setupButtons (ethQuery) {
const accountButton = document.getElementById('action-button-1')
accountButton.addEventListener('click', async () => {
const accounts = await ethQuery.accounts()
window.METAMASK_ACCOUNT = accounts[0] || 'locked'
logToDom(accounts.length ? accounts[0] : 'LOCKED or undefined', 'account')
})
const txButton = document.getElementById('action-button-2')
txButton.addEventListener('click', async () => {
if (!window.METAMASK_ACCOUNT || window.METAMASK_ACCOUNT === 'locked') return
const txHash = await ethQuery.sendTransaction({
from: window.METAMASK_ACCOUNT,
to: window.METAMASK_ACCOUNT,
data: '',
})
logToDom(txHash, 'cb-value')
})
}

View File

@ -1,17 +0,0 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<script src="http://localhost:9001/metamascara.js"></script>
<title>Nifty Wallet ZeroClient Example</title>
</head>
<body>
<button id="action-button-1">GET ACCOUNT</button>
<div id="account"></div>
<button id="action-button-2">SEND TRANSACTION</button>
<div id="cb-value" ></div>
<script src="./app.js"></script>
</body>
</html>

View File

@ -1,31 +0,0 @@
const express = require('express')
const path = require('path')
const createMetamascaraServer = require('../server/')
const createBundle = require('../server/util').createBundle
const serveBundle = require('../server/util').serveBundle
//
// Iframe Server
//
const mascaraServer = createMetamascaraServer()
// start the server
const mascaraPort = 9001
mascaraServer.listen(mascaraPort)
console.log(`Mascara service listening on port ${mascaraPort}`)
//
// Dapp Server
//
const dappServer = express()
// serve dapp bundle
serveBundle(dappServer, '/app.js', createBundle(require.resolve('./app.js')))
dappServer.use(express.static(path.join(__dirname, '/app/')))
// start the server
const dappPort = '9002'
dappServer.listen(dappPort)
console.log(`Dapp listening on port ${dappPort}`)

View File

@ -1,20 +0,0 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Nifty Wallet ZeroClient Iframe</title>
<meta name="description" content="MetaMask ZeroClient">
<meta name="author" content="MetaMask">
<!--[if lt IE 9]>
<script src="http://html5shiv.googlecode.com/svn/trunk/html5.js"></script>
<![endif]-->
</head>
<body>
Hello! I am the Nifty Wallet iframe.
<script src="./proxy.js"></script>
</body>
</html>

View File

@ -1,21 +0,0 @@
const path = require('path')
const express = require('express')
const compression = require('compression')
module.exports = createMetamascaraServer
function createMetamascaraServer () {
// setup server
const server = express()
server.use(compression())
// serve assets
server.use(express.static(path.join(__dirname, '/../ui/'), { setHeaders: (res) => res.set('X-Frame-Options', 'DENY') }))
server.use(express.static(path.join(__dirname, '/../../dist/mascara')))
server.use(express.static(path.join(__dirname, '/../proxy')))
return server
}

View File

@ -1,47 +0,0 @@
const browserify = require('browserify')
const watchify = require('watchify')
module.exports = {
serveBundle,
createBundle,
}
function serveBundle (server, path, bundle) {
server.get(path, function (req, res) {
res.setHeader('Content-Type', 'application/javascript; charset=UTF-8')
res.send(bundle.latest)
})
}
function createBundle (entryPoint) {
var bundleContainer = {}
var bundler = browserify({
entries: [entryPoint],
cache: {},
packageCache: {},
plugin: [watchify],
})
.transform('babelify')
.transform('uglifyify', { global: true })
bundler.on('update', bundle)
bundle()
return bundleContainer
function bundle () {
bundler.bundle(function (err, result) {
if (err) {
console.log(`Bundle failed! (${entryPoint})`)
console.error(err)
return
}
console.log(`Bundle updated! (${entryPoint})`)
bundleContainer.latest = result.toString()
})
}
}

View File

@ -1,198 +0,0 @@
import React, { Component } from 'react'
import PropTypes from 'prop-types'
import classnames from 'classnames'
import {connect} from 'react-redux'
import {qrcode} from 'qrcode-npm'
import copyToClipboard from 'copy-to-clipboard'
import ShapeShiftForm from '../shapeshift-form'
import {buyEth, showAccountDetail} from '../../../../ui/app/actions'
const OPTION_VALUES = {
COINBASE: 'coinbase',
SHAPESHIFT: 'shapeshift',
QR_CODE: 'qr_code',
}
const OPTIONS = [
{
name: 'Direct Deposit',
value: OPTION_VALUES.QR_CODE,
},
{
name: 'Buy with Dollars',
value: OPTION_VALUES.COINBASE,
},
{
name: 'Buy with Cryptos',
value: OPTION_VALUES.SHAPESHIFT,
},
]
class BuyEtherWidget extends Component {
static propTypes = {
address: PropTypes.string,
skipText: PropTypes.string,
className: PropTypes.string,
onSkip: PropTypes.func,
goToCoinbase: PropTypes.func,
showAccountDetail: PropTypes.func,
};
state = {
selectedOption: OPTION_VALUES.QR_CODE,
};
copyToClipboard = () => {
const { address } = this.props
this.setState({ justCopied: true }, () => copyToClipboard(address))
setTimeout(() => this.setState({ justCopied: false }), 1000)
}
renderSkip () {
const {showAccountDetail, address, skipText, onSkip} = this.props
return (
<div
className="buy-ether__do-it-later"
onClick={() => {
if (onSkip) return onSkip()
showAccountDetail(address)
}}
>
{skipText || 'Do it later'}
</div>
)
}
renderCoinbaseLogo () {
return (
<svg width="140px" height="49px" viewBox="0 0 579 126" version="1.1">
<g id="Page-1" stroke="none" strokeWidth={1} fill="none" fillRule="evenodd">
<g id="Imported-Layers" fill="#0081C9">
<path d="M37.752,125.873 C18.824,125.873 0.369,112.307 0.369,81.549 C0.369,50.79 18.824,37.382 37.752,37.382 C47.059,37.382 54.315,39.749 59.52,43.219 L53.841,55.68 C50.371,53.156 45.166,51.579 39.961,51.579 C28.604,51.579 18.193,60.57 18.193,81.391 C18.193,102.212 28.919,111.361 39.961,111.361 C45.166,111.361 50.371,109.783 53.841,107.26 L59.52,120.036 C54.157,123.664 47.059,125.873 37.752,125.873" id="Fill-1" />
<path d="M102.898,125.873 C78.765,125.873 65.515,106.786 65.515,81.549 C65.515,56.311 78.765,37.382 102.898,37.382 C127.032,37.382 140.282,56.311 140.282,81.549 C140.282,106.786 127.032,125.873 102.898,125.873 L102.898,125.873 Z M102.898,51.105 C89.491,51.105 82.866,63.093 82.866,81.391 C82.866,99.688 89.491,111.834 102.898,111.834 C116.306,111.834 122.931,99.688 122.931,81.391 C122.931,63.093 116.306,51.105 102.898,51.105 L102.898,51.105 Z" id="Fill-2" />
<path d="M163.468,23.659 C157.79,23.659 153.215,19.243 153.215,13.88 C153.215,8.517 157.79,4.1 163.468,4.1 C169.146,4.1 173.721,8.517 173.721,13.88 C173.721,19.243 169.146,23.659 163.468,23.659 L163.468,23.659 Z M154.793,39.118 L172.144,39.118 L172.144,124.138 L154.793,124.138 L154.793,39.118 Z" id="Fill-3" />
<path d="M240.443,124.137 L240.443,67.352 C240.443,57.415 234.449,51.263 222.619,51.263 C216.31,51.263 210.473,52.367 207.003,53.787 L207.003,124.137 L189.81,124.137 L189.81,43.376 C198.328,39.906 209.212,37.382 222.461,37.382 C246.28,37.382 257.794,47.793 257.794,65.775 L257.794,124.137 L240.443,124.137" id="Fill-4" />
<path d="M303.536,125.873 C292.494,125.873 281.611,123.191 274.986,119.879 L274.986,0.314 L292.179,0.314 L292.179,41.326 C296.28,39.433 302.905,37.856 308.741,37.856 C330.667,37.856 345.494,53.629 345.494,79.656 C345.494,111.676 328.931,125.873 303.536,125.873 L303.536,125.873 Z M305.744,51.263 C301.012,51.263 295.491,52.367 292.179,54.103 L292.179,109.941 C294.703,111.045 299.593,112.149 304.482,112.149 C318.205,112.149 328.301,102.685 328.301,80.918 C328.301,62.305 319.467,51.263 305.744,51.263 L305.744,51.263 Z" id="Fill-5" />
<path d="M392.341,125.873 C367.892,125.873 355.589,115.935 355.589,99.215 C355.589,75.555 380.826,71.296 406.537,69.876 L406.537,64.513 C406.537,53.787 399.439,50.001 388.555,50.001 C380.511,50.001 370.731,52.525 365.053,55.207 L360.636,43.376 C367.419,40.379 378.933,37.382 390.29,37.382 C410.638,37.382 422.942,45.269 422.942,66.248 L422.942,119.879 C416.79,123.191 404.329,125.873 392.341,125.873 L392.341,125.873 Z M406.537,81.391 C389.186,82.337 371.835,83.757 371.835,98.9 C371.835,107.89 378.776,113.411 391.868,113.411 C397.389,113.411 403.856,112.465 406.537,111.203 L406.537,81.391 L406.537,81.391 Z" id="Fill-6" />
<path d="M461.743,125.873 C451.806,125.873 441.395,123.191 435.244,119.879 L441.08,106.629 C445.496,109.31 454.803,112.149 461.27,112.149 C470.576,112.149 476.728,107.575 476.728,100.477 C476.728,92.748 470.261,89.751 461.586,86.596 C450.228,82.337 437.452,77.132 437.452,61.201 C437.452,47.162 448.336,37.382 467.264,37.382 C477.517,37.382 486.035,39.906 492.029,43.376 L486.665,55.364 C482.88,52.998 475.309,50.317 469.157,50.317 C460.166,50.317 455.118,55.049 455.118,61.201 C455.118,68.93 461.428,71.611 469.788,74.766 C481.618,79.183 494.71,84.072 494.71,100.635 C494.71,115.935 483.038,125.873 461.743,125.873" id="Fill-7" />
<path d="M578.625,81.233 L522.155,89.12 C523.89,104.42 533.828,112.149 548.182,112.149 C556.699,112.149 565.848,110.099 571.684,106.944 L576.732,119.879 C570.107,123.349 558.75,125.873 547.078,125.873 C520.262,125.873 505.277,108.679 505.277,81.549 C505.277,55.522 519.789,37.382 543.607,37.382 C565.69,37.382 578.782,51.894 578.782,74.766 C578.782,76.816 578.782,79.025 578.625,81.233 L578.625,81.233 Z M543.292,50.001 C530.042,50.001 521.367,60.097 521.051,77.763 L562.22,72.084 C562.062,57.257 554.649,50.001 543.292,50.001 L543.292,50.001 Z" id="Fill-8" />
</g>
</g>
</svg>
)
}
renderCoinbaseForm () {
const {goToCoinbase, address} = this.props
return (
<div className="buy-ether__action-content-wrapper">
<div>{this.renderCoinbaseLogo()}</div>
<div className="buy-ether__body-text">Coinbase is the worlds most popular way to buy and sell bitcoin, ethereum, and litecoin.</div>
<a className="first-time-flow__link buy-ether__faq-link">What is Ethereum?</a>
<div className="buy-ether__buttons">
<button
className="first-time-flow__button"
onClick={() => goToCoinbase(address)}
>
Buy
</button>
</div>
</div>
)
}
renderContent () {
const { address } = this.props
const { justCopied } = this.state
const qrImage = qrcode(4, 'M')
qrImage.addData(address)
qrImage.make()
switch (this.state.selectedOption) {
case OPTION_VALUES.COINBASE:
return this.renderCoinbaseForm()
case OPTION_VALUES.SHAPESHIFT:
return (
<div className="buy-ether__action-content-wrapper">
<div className="shapeshift-logo" />
<div className="buy-ether__body-text">
Trade any leading blockchain asset for any other. Protection by Design. No Account Needed.
</div>
<ShapeShiftForm btnClass="first-time-flow__button" />
</div>
)
case OPTION_VALUES.QR_CODE:
return (
<div className="buy-ether__action-content-wrapper">
<div dangerouslySetInnerHTML={{ __html: qrImage.createTableTag(4) }} />
<div className="buy-ether__body-text">Deposit Ether directly into your account.</div>
<div className="buy-ether__small-body-text">(This is the account address that Nifty Wallet created for you to recieve funds.)</div>
<div className="buy-ether__buttons">
<button
className="first-time-flow__button"
onClick={this.copyToClipboard}
disabled={justCopied}
>
{ justCopied ? 'Copied' : 'Copy' }
</button>
</div>
</div>
)
default:
return null
}
}
render () {
const { className = '' } = this.props
const { selectedOption } = this.state
return (
<div className={`${className} buy-ether__content-wrapper`}>
<div className="buy-ether__content-headline-wrapper">
<div className="buy-ether__content-headline">Deposit Options</div>
{this.renderSkip()}
</div>
<div className="buy-ether__content">
<div className="buy-ether__side-panel">
{OPTIONS.map(({ name, value }) => (
<div
key={value}
className={classnames('buy-ether__side-panel-item', {
'buy-ether__side-panel-item--selected': value === selectedOption,
})}
onClick={() => this.setState({ selectedOption: value })}
>
<div className="buy-ether__side-panel-item-name">{name}</div>
{value === selectedOption && (
<svg viewBox="0 0 574 1024" id="si-ant-right" width="15px" height="15px">
<path d="M10 9Q0 19 0 32t10 23l482 457L10 969Q0 979 0 992t10 23q10 9 24 9t24-9l506-480q10-10 10-23t-10-23L58 9Q48 0 34 0T10 9z" />
</svg>
)}
</div>
))}
</div>
<div className="buy-ether__action-content">
{this.renderContent()}
</div>
</div>
</div>
)
}
}
export default connect(
({ metamask: { selectedAddress } }) => ({
address: selectedAddress,
}),
dispatch => ({
goToCoinbase: address => dispatch(buyEth({ network: '1', address, amount: 0 })),
showAccountDetail: address => dispatch(showAccountDetail(address)),
}),
)(BuyEtherWidget)

View File

@ -1,26 +0,0 @@
import React, { Component } from 'react'
import PropTypes from 'prop-types'
export default class Breadcrumbs extends Component {
static propTypes = {
total: PropTypes.number,
currentIndex: PropTypes.number,
};
render () {
const {total, currentIndex} = this.props
return (
<div className="breadcrumbs">
{Array(total).fill().map((_, i) => (
<div
key={i}
className="breadcrumb"
style={{backgroundColor: i === currentIndex ? '#D8D8D8' : '#FFFFFF'}}
/>
))}
</div>
)
}
}

View File

@ -1,200 +0,0 @@
import React, { Component } from 'react'
import PropTypes from 'prop-types'
import classnames from 'classnames'
import {connect} from 'react-redux'
import {qrcode} from 'qrcode-npm'
import copyToClipboard from 'copy-to-clipboard'
import ShapeShiftForm from '../shapeshift-form'
import Identicon from '../../../../ui/app/components/identicon'
import {buyEth, showAccountDetail} from '../../../../ui/app/actions'
class BuyEtherScreen extends Component {
static OPTION_VALUES = {
COINBASE: 'coinbase',
SHAPESHIFT: 'shapeshift',
QR_CODE: 'qr_code',
};
static OPTIONS = [
{
name: 'Direct Deposit',
value: BuyEtherScreen.OPTION_VALUES.QR_CODE,
},
{
name: 'Buy with Dollars',
value: BuyEtherScreen.OPTION_VALUES.COINBASE,
},
{
name: 'Buy with Cryptos',
value: BuyEtherScreen.OPTION_VALUES.SHAPESHIFT,
},
];
static propTypes = {
address: PropTypes.string,
goToCoinbase: PropTypes.func.isRequired,
showAccountDetail: PropTypes.func.isRequired,
}
state = {
selectedOption: BuyEtherScreen.OPTION_VALUES.QR_CODE,
justCopied: false,
}
copyToClipboard = () => {
const { address } = this.props
this.setState({ justCopied: true }, () => copyToClipboard(address))
setTimeout(() => this.setState({ justCopied: false }), 1000)
}
renderSkip () {
const {showAccountDetail, address} = this.props
return (
<div
className="buy-ether__do-it-later"
onClick={() => showAccountDetail(address)}
>
Do it later
</div>
)
}
renderCoinbaseLogo () {
return (
<svg width="140px" height="49px" viewBox="0 0 579 126" version="1.1">
<g id="Page-1" stroke="none" strokeWidth={1} fill="none" fillRule="evenodd">
<g id="Imported-Layers" fill="#0081C9">
<path d="M37.752,125.873 C18.824,125.873 0.369,112.307 0.369,81.549 C0.369,50.79 18.824,37.382 37.752,37.382 C47.059,37.382 54.315,39.749 59.52,43.219 L53.841,55.68 C50.371,53.156 45.166,51.579 39.961,51.579 C28.604,51.579 18.193,60.57 18.193,81.391 C18.193,102.212 28.919,111.361 39.961,111.361 C45.166,111.361 50.371,109.783 53.841,107.26 L59.52,120.036 C54.157,123.664 47.059,125.873 37.752,125.873" id="Fill-1" />
<path d="M102.898,125.873 C78.765,125.873 65.515,106.786 65.515,81.549 C65.515,56.311 78.765,37.382 102.898,37.382 C127.032,37.382 140.282,56.311 140.282,81.549 C140.282,106.786 127.032,125.873 102.898,125.873 L102.898,125.873 Z M102.898,51.105 C89.491,51.105 82.866,63.093 82.866,81.391 C82.866,99.688 89.491,111.834 102.898,111.834 C116.306,111.834 122.931,99.688 122.931,81.391 C122.931,63.093 116.306,51.105 102.898,51.105 L102.898,51.105 Z" id="Fill-2" />
<path d="M163.468,23.659 C157.79,23.659 153.215,19.243 153.215,13.88 C153.215,8.517 157.79,4.1 163.468,4.1 C169.146,4.1 173.721,8.517 173.721,13.88 C173.721,19.243 169.146,23.659 163.468,23.659 L163.468,23.659 Z M154.793,39.118 L172.144,39.118 L172.144,124.138 L154.793,124.138 L154.793,39.118 Z" id="Fill-3" />
<path d="M240.443,124.137 L240.443,67.352 C240.443,57.415 234.449,51.263 222.619,51.263 C216.31,51.263 210.473,52.367 207.003,53.787 L207.003,124.137 L189.81,124.137 L189.81,43.376 C198.328,39.906 209.212,37.382 222.461,37.382 C246.28,37.382 257.794,47.793 257.794,65.775 L257.794,124.137 L240.443,124.137" id="Fill-4" />
<path d="M303.536,125.873 C292.494,125.873 281.611,123.191 274.986,119.879 L274.986,0.314 L292.179,0.314 L292.179,41.326 C296.28,39.433 302.905,37.856 308.741,37.856 C330.667,37.856 345.494,53.629 345.494,79.656 C345.494,111.676 328.931,125.873 303.536,125.873 L303.536,125.873 Z M305.744,51.263 C301.012,51.263 295.491,52.367 292.179,54.103 L292.179,109.941 C294.703,111.045 299.593,112.149 304.482,112.149 C318.205,112.149 328.301,102.685 328.301,80.918 C328.301,62.305 319.467,51.263 305.744,51.263 L305.744,51.263 Z" id="Fill-5" />
<path d="M392.341,125.873 C367.892,125.873 355.589,115.935 355.589,99.215 C355.589,75.555 380.826,71.296 406.537,69.876 L406.537,64.513 C406.537,53.787 399.439,50.001 388.555,50.001 C380.511,50.001 370.731,52.525 365.053,55.207 L360.636,43.376 C367.419,40.379 378.933,37.382 390.29,37.382 C410.638,37.382 422.942,45.269 422.942,66.248 L422.942,119.879 C416.79,123.191 404.329,125.873 392.341,125.873 L392.341,125.873 Z M406.537,81.391 C389.186,82.337 371.835,83.757 371.835,98.9 C371.835,107.89 378.776,113.411 391.868,113.411 C397.389,113.411 403.856,112.465 406.537,111.203 L406.537,81.391 L406.537,81.391 Z" id="Fill-6" />
<path d="M461.743,125.873 C451.806,125.873 441.395,123.191 435.244,119.879 L441.08,106.629 C445.496,109.31 454.803,112.149 461.27,112.149 C470.576,112.149 476.728,107.575 476.728,100.477 C476.728,92.748 470.261,89.751 461.586,86.596 C450.228,82.337 437.452,77.132 437.452,61.201 C437.452,47.162 448.336,37.382 467.264,37.382 C477.517,37.382 486.035,39.906 492.029,43.376 L486.665,55.364 C482.88,52.998 475.309,50.317 469.157,50.317 C460.166,50.317 455.118,55.049 455.118,61.201 C455.118,68.93 461.428,71.611 469.788,74.766 C481.618,79.183 494.71,84.072 494.71,100.635 C494.71,115.935 483.038,125.873 461.743,125.873" id="Fill-7" />
<path d="M578.625,81.233 L522.155,89.12 C523.89,104.42 533.828,112.149 548.182,112.149 C556.699,112.149 565.848,110.099 571.684,106.944 L576.732,119.879 C570.107,123.349 558.75,125.873 547.078,125.873 C520.262,125.873 505.277,108.679 505.277,81.549 C505.277,55.522 519.789,37.382 543.607,37.382 C565.69,37.382 578.782,51.894 578.782,74.766 C578.782,76.816 578.782,79.025 578.625,81.233 L578.625,81.233 Z M543.292,50.001 C530.042,50.001 521.367,60.097 521.051,77.763 L562.22,72.084 C562.062,57.257 554.649,50.001 543.292,50.001 L543.292,50.001 Z" id="Fill-8" />
</g>
</g>
</svg>
)
}
renderCoinbaseForm () {
const {goToCoinbase, address} = this.props
return (
<div className="buy-ether__action-content-wrapper">
<div>{this.renderCoinbaseLogo()}</div>
<div className="buy-ether__body-text">Coinbase is the worlds most popular way to buy and sell bitcoin, ethereum, and litecoin.</div>
<a className="first-time-flow__link buy-ether__faq-link">What is Ethereum?</a>
<div className="buy-ether__buttons">
<button
className="first-time-flow__button"
onClick={() => goToCoinbase(address)}
>
Buy
</button>
</div>
</div>
)
}
renderContent () {
const { OPTION_VALUES } = BuyEtherScreen
const { address } = this.props
const { justCopied } = this.state
const qrImage = qrcode(4, 'M')
qrImage.addData(address)
qrImage.make()
switch (this.state.selectedOption) {
case OPTION_VALUES.COINBASE:
return this.renderCoinbaseForm()
case OPTION_VALUES.SHAPESHIFT:
return (
<div className="buy-ether__action-content-wrapper">
<div className="shapeshift-logo" />
<div className="buy-ether__body-text">
Trade any leading blockchain asset for any other. Protection by Design. No Account Needed.
</div>
<ShapeShiftForm btnClass="first-time-flow__button" />
</div>
)
case OPTION_VALUES.QR_CODE:
return (
<div className="buy-ether__action-content-wrapper">
<div dangerouslySetInnerHTML={{ __html: qrImage.createTableTag(4) }} />
<div className="buy-ether__body-text">Deposit Ether directly into your account.</div>
<div className="buy-ether__small-body-text">(This is the account address that Nifty Wallet created for you to recieve funds.)</div>
<div className="buy-ether__buttons">
<button
className="first-time-flow__button"
onClick={this.copyToClipboard}
disabled={justCopied}
>
{ justCopied ? 'Copied' : 'Copy' }
</button>
</div>
</div>
)
default:
return null
}
}
render () {
const { OPTIONS } = BuyEtherScreen
const { selectedOption } = this.state
return (
<div className="buy-ether">
<Identicon address={this.props.address} diameter={70} />
<div className="buy-ether__title">Deposit Ether</div>
<div className="buy-ether__body-text">
Nifty Wallet works best if you have Ether in your account to pay for transaction gas fees and more. To get Ether, choose from one of these methods.
</div>
<div className="buy-ether__content-wrapper">
<div className="buy-ether__content-headline-wrapper">
<div className="buy-ether__content-headline">Deposit Options</div>
{this.renderSkip()}
</div>
<div className="buy-ether__content">
<div className="buy-ether__side-panel">
{OPTIONS.map(({ name, value }) => (
<div
key={value}
className={classnames('buy-ether__side-panel-item', {
'buy-ether__side-panel-item--selected': value === selectedOption,
})}
onClick={() => this.setState({ selectedOption: value })}
>
<div className="buy-ether__side-panel-item-name">{name}</div>
{value === selectedOption && (
<svg viewBox="0 0 574 1024" id="si-ant-right" width="15px" height="15px">
<path d="M10 9Q0 19 0 32t10 23l482 457L10 969Q0 979 0 992t10 23q10 9 24 9t24-9l506-480q10-10 10-23t-10-23L58 9Q48 0 34 0T10 9z" />
</svg>
)}
</div>
))}
</div>
<div className="buy-ether__action-content">
{this.renderContent()}
</div>
</div>
</div>
</div>
)
}
}
export default connect(
({ metamask: { selectedAddress } }) => ({
address: selectedAddress,
}),
dispatch => ({
goToCoinbase: address => dispatch(buyEth({ network: '1', address, amount: 0 })),
showAccountDetail: address => dispatch(showAccountDetail(address)),
}),
)(BuyEtherScreen)

View File

@ -1,163 +0,0 @@
import React, { Component } from 'react'
import PropTypes from 'prop-types'
import { connect } from 'react-redux'
import { withRouter } from 'react-router-dom'
import classnames from 'classnames'
import shuffle from 'lodash.shuffle'
import { compose } from 'recompose'
import Identicon from '../../../../ui/app/components/identicon'
import { confirmSeedWords, showModal } from '../../../../ui/app/actions'
import Breadcrumbs from './breadcrumbs'
import LoadingScreen from './loading-screen'
import { DEFAULT_ROUTE, INITIALIZE_BACKUP_PHRASE_ROUTE } from '../../../../ui/app/routes'
class ConfirmSeedScreen extends Component {
static propTypes = {
isLoading: PropTypes.bool,
address: PropTypes.string,
seedWords: PropTypes.string,
confirmSeedWords: PropTypes.func,
history: PropTypes.object,
openBuyEtherModal: PropTypes.func,
};
static defaultProps = {
seedWords: '',
}
constructor (props) {
super(props)
const { seedWords } = props
this.state = {
selectedSeeds: [],
shuffledSeeds: seedWords && shuffle(seedWords.split(' ')) || [],
}
}
// eslint-disable-next-line camelcase
UNSAFE_componentWillMount () {
const { seedWords, history } = this.props
if (!seedWords) {
history.push(DEFAULT_ROUTE)
}
}
handleClick () {
const { confirmSeedWords, history, openBuyEtherModal } = this.props
confirmSeedWords()
.then(() => {
history.push(DEFAULT_ROUTE)
openBuyEtherModal()
})
}
render () {
const { seedWords, history } = this.props
const { selectedSeeds, shuffledSeeds } = this.state
const isValid = seedWords === selectedSeeds.map(([_, seed]) => seed).join(' ')
return (
<div className="first-time-flow">
{
this.props.isLoading
? <LoadingScreen loadingMessage="Creating your new account" />
: (
<div className="first-view-main-wrapper">
<div className="first-view-main">
<div className="backup-phrase">
<a
className="backup-phrase__back-button"
onClick={e => {
e.preventDefault()
history.push(INITIALIZE_BACKUP_PHRASE_ROUTE)
}}
href="#"
>
{`< Back`}
</a>
<Identicon address={this.props.address} diameter={70} />
<div className="backup-phrase__content-wrapper">
<div>
<div className="backup-phrase__title">
Confirm your Secret Backup Phrase
</div>
<div className="backup-phrase__body-text">
Please select each phrase in order to make sure it is correct.
</div>
<div className="backup-phrase__confirm-secret">
{selectedSeeds.map(([_, word], i) => (
<button
key={i}
className="backup-phrase__confirm-seed-option"
>
{word}
</button>
))}
</div>
<div className="backup-phrase__confirm-seed-options">
{shuffledSeeds.map((word, i) => {
const isSelected = selectedSeeds
.filter(([index, seed]) => seed === word && index === i)
.length
return (
<button
key={i}
className={classnames('backup-phrase__confirm-seed-option', {
'backup-phrase__confirm-seed-option--selected': isSelected,
'backup-phrase__confirm-seed-option--unselected': !isSelected,
})}
onClick={() => {
if (!isSelected) {
this.setState({
selectedSeeds: [...selectedSeeds, [i, word]],
})
} else {
this.setState({
selectedSeeds: selectedSeeds
.filter(([index, seed]) => !(seed === word && index === i)),
})
}
}}
>
{word}
</button>
)
})}
</div>
<button
className="first-time-flow__button"
onClick={() => isValid && this.handleClick()}
disabled={!isValid}
>
Confirm
</button>
</div>
</div>
<Breadcrumbs total={3} currentIndex={1} />
</div>
</div>
</div>
)
}
</div>
)
}
}
export default compose(
withRouter,
connect(
({ metamask: { selectedAddress, seedWords }, appState: { isLoading } }) => ({
seedWords,
isLoading,
address: selectedAddress,
}),
dispatch => ({
confirmSeedWords: () => dispatch(confirmSeedWords()),
openBuyEtherModal: () => dispatch(showModal({ name: 'DEPOSIT_ETHER'})),
}),
),
)(ConfirmSeedScreen)

View File

@ -1,216 +0,0 @@
import React, { Component } from 'react'
import PropTypes from 'prop-types'
import {connect} from 'react-redux'
import { withRouter } from 'react-router-dom'
import { compose } from 'recompose'
import { createNewVaultAndKeychain } from '../../../../ui/app/actions'
import Breadcrumbs from './breadcrumbs'
import EventEmitter from 'events'
import classnames from 'classnames'
import {
INITIALIZE_UNIQUE_IMAGE_ROUTE,
INITIALIZE_IMPORT_WITH_SEED_PHRASE_ROUTE,
INITIALIZE_NOTICE_ROUTE,
} from '../../../../ui/app/routes'
import TextField from '../../../../ui/app/components/text-field'
class CreatePasswordScreen extends Component {
static contextTypes = {
t: PropTypes.func,
}
static propTypes = {
isLoading: PropTypes.bool.isRequired,
createAccount: PropTypes.func.isRequired,
history: PropTypes.object.isRequired,
isInitialized: PropTypes.bool,
isUnlocked: PropTypes.bool,
isMascara: PropTypes.bool.isRequired,
}
state = {
password: '',
confirmPassword: '',
passwordError: null,
confirmPasswordError: null,
}
constructor (props) {
super(props)
this.animationEventEmitter = new EventEmitter()
}
// eslint-disable-next-line camelcase
UNSAFE_componentWillMount () {
const { isInitialized, history } = this.props
if (isInitialized) {
history.push(INITIALIZE_NOTICE_ROUTE)
}
}
isValid () {
const { password, confirmPassword } = this.state
if (!password || !confirmPassword) {
return false
}
if (password.length < 8) {
return false
}
return password === confirmPassword
}
createAccount = (event) => {
event.preventDefault()
if (!this.isValid()) {
return
}
const { password } = this.state
const { createAccount, history } = this.props
this.setState({ isLoading: true })
createAccount(password)
.then(() => history.push(INITIALIZE_UNIQUE_IMAGE_ROUTE))
}
handlePasswordChange (password) {
const { confirmPassword } = this.state
let confirmPasswordError = null
let passwordError = null
if (password && password.length < 8) {
passwordError = this.context.t('passwordNotLongEnough')
}
if (confirmPassword && password !== confirmPassword) {
confirmPasswordError = this.context.t('passwordsDontMatch')
}
this.setState({ password, passwordError, confirmPasswordError })
}
handleConfirmPasswordChange (confirmPassword) {
const { password } = this.state
let confirmPasswordError = null
if (password !== confirmPassword) {
confirmPasswordError = this.context.t('passwordsDontMatch')
}
this.setState({ confirmPassword, confirmPasswordError })
}
render () {
const { history, isMascara } = this.props
const { passwordError, confirmPasswordError } = this.state
const { t } = this.context
return (
<div className={classnames({ 'first-view-main-wrapper': !isMascara })}>
<div className={classnames({
'first-view-main': !isMascara,
'first-view-main__mascara': isMascara,
})}>
{isMascara && <div className="mascara-info first-view-phone-invisible">
<div className="info">
Nifty Wallet is a secure identity vault for Ethereum.
</div>
<div className="info">
It allows you to hold ether & tokens, and interact with decentralized applications.
</div>
</div>}
<form className="create-password">
<div className="create-password__title">
Create Password
</div>
<TextField
id="create-password"
label={t('newPassword')}
type="password"
className="first-time-flow__input"
value={this.state.password}
onChange={event => this.handlePasswordChange(event.target.value)}
error={passwordError}
autoFocus
autoComplete="new-password"
margin="normal"
fullWidth
largeLabel
/>
<TextField
id="confirm-password"
label={t('confirmPassword')}
type="password"
className="first-time-flow__input"
value={this.state.confirmPassword}
onChange={event => this.handleConfirmPasswordChange(event.target.value)}
error={confirmPasswordError}
autoComplete="confirm-password"
margin="normal"
fullWidth
largeLabel
/>
<button
className="first-time-flow__button"
disabled={!this.isValid()}
onClick={this.createAccount}
>
Create
</button>
<a
href=""
className="first-time-flow__link create-password__import-link"
onClick={e => {
e.preventDefault()
history.push(INITIALIZE_IMPORT_WITH_SEED_PHRASE_ROUTE)
}}
>
Import with seed phrase
</a>
{ /* }
<a
href=""
className="first-time-flow__link create-password__import-link"
onClick={e => {
e.preventDefault()
history.push(INITIALIZE_IMPORT_ACCOUNT_ROUTE)
}}
>
Import an account
</a>
{ */ }
<Breadcrumbs total={3} currentIndex={0} />
</form>
</div>
</div>
)
}
}
const mapStateToProps = ({ metamask, appState }) => {
const { isInitialized, isUnlocked, isMascara, noActiveNotices } = metamask
const { isLoading } = appState
return {
isLoading,
isInitialized,
isUnlocked,
isMascara,
noActiveNotices,
}
}
export default compose(
withRouter,
connect(
mapStateToProps,
dispatch => ({
createAccount: password => dispatch(createNewVaultAndKeychain(password)),
}),
),
)(CreatePasswordScreen)

View File

@ -1,214 +0,0 @@
import React, { Component } from 'react'
import PropTypes from 'prop-types'
import {connect} from 'react-redux'
import classnames from 'classnames'
import LoadingScreen from './loading-screen'
import {importNewAccount, hideWarning} from '../../../../ui/app/actions'
class Input extends Component {
static propTypes = {
label: PropTypes.string.isRequired,
placeholder: PropTypes.string.isRequired,
type: PropTypes.string.isRequired,
errorMessage: PropTypes.string.isRequired,
onChange: PropTypes.func.isRequired,
}
render () {
const {label, type, placeholder, errorMessage, onChange} = this.props
return (
<div className="import-account__input-wrapper">
<div className="import-account__input-label">{label}</div>
<input
type={type}
placeholder={placeholder}
className={classnames('first-time-flow__input import-account__input', {
'first-time-flow__input--error': errorMessage,
})}
onChange={onChange}
/>
<div className="import-account__input-error-message">{errorMessage}</div>
</div>
)
}
}
class ImportAccountScreen extends Component {
static OPTIONS = {
PRIVATE_KEY: 'private_key',
JSON_FILE: 'json_file',
};
static propTypes = {
warning: PropTypes.string,
back: PropTypes.func.isRequired,
next: PropTypes.func.isRequired,
importNewAccount: PropTypes.func.isRequired,
hideWarning: PropTypes.func.isRequired,
isLoading: PropTypes.bool.isRequired,
};
state = {
selectedOption: ImportAccountScreen.OPTIONS.PRIVATE_KEY,
privateKey: '',
jsonFile: {},
}
isValid () {
const { OPTIONS } = ImportAccountScreen
const { privateKey, jsonFile, password } = this.state
switch (this.state.selectedOption) {
case OPTIONS.JSON_FILE:
return Boolean(jsonFile && password)
case OPTIONS.PRIVATE_KEY:
default:
return Boolean(privateKey)
}
}
onClick = () => {
const { OPTIONS } = ImportAccountScreen
const { importNewAccount, next } = this.props
const { privateKey, jsonFile, password } = this.state
switch (this.state.selectedOption) {
case OPTIONS.JSON_FILE:
return importNewAccount('JSON File', [ jsonFile, password ])
// JS runtime requires caught rejections but failures are handled by Redux
.catch()
.then(next)
case OPTIONS.PRIVATE_KEY:
default:
return importNewAccount('Private Key', [ privateKey ])
// JS runtime requires caught rejections but failures are handled by Redux
.catch()
.then(next)
}
}
renderPrivateKey () {
return Input({
label: 'Add Private Key String',
placeholder: 'Enter private key',
onChange: e => this.setState({ privateKey: e.target.value }),
errorMessage: this.props.warning && 'Something went wrong. Please make sure your private key is correct.',
})
}
renderJsonFile () {
const { jsonFile: { name } } = this.state
const { warning } = this.props
return (
<div className="">
<div className="import-account__input-wrapper">
<div className="import-account__input-label">Upload File</div>
<div className="import-account__file-picker-wrapper">
<input
type="file"
id="file"
className="import-account__file-input"
onChange={e => this.setState({ jsonFile: e.target.files[0] })}
/>
<label
htmlFor="file"
className={classnames('import-account__file-input-label', {
'import-account__file-input-label--error': warning,
})}
>
Choose File
</label>
<div className="import-account__file-name">{name}</div>
</div>
<div className="import-account__input-error-message">
{warning && 'Something went wrong. Please make sure your JSON file is properly formatted.'}
</div>
</div>
{Input({
label: 'Enter Password',
placeholder: 'Enter Password',
type: 'password',
onChange: e => this.setState({ password: e.target.value }),
errorMessage: warning && 'Please make sure your password is correct.',
})}
</div>
)
}
renderContent () {
const { OPTIONS } = ImportAccountScreen
switch (this.state.selectedOption) {
case OPTIONS.JSON_FILE:
return this.renderJsonFile()
case OPTIONS.PRIVATE_KEY:
default:
return this.renderPrivateKey()
}
}
render () {
const { OPTIONS } = ImportAccountScreen
const { selectedOption } = this.state
return this.props.isLoading
? <LoadingScreen loadingMessage="Creating your new account" />
: (
<div className="import-account">
<a
className="import-account__back-button"
onClick={e => {
e.preventDefault()
this.props.back()
}}
href="#"
>
{`< Back`}
</a>
<div className="import-account__title">
Import an Account
</div>
<div className="import-account__selector-label">
How would you like to import your account?
</div>
<select
className="import-account__dropdown"
value={selectedOption}
onChange={e => {
this.setState({ selectedOption: e.target.value })
this.props.hideWarning()
}}
>
<option value={OPTIONS.PRIVATE_KEY}>Private Key</option>
<option value={OPTIONS.JSON_FILE}>JSON File</option>
</select>
{this.renderContent()}
<button
className="first-time-flow__button"
disabled={!this.isValid()}
onClick={this.onClick}
>
Import
</button>
<a
href="https://github.com/MetaMask/faq/blob/master/README.md#q-i-cant-use-the-import-feature-for-uploading-a-json-file-the-window-keeps-closing-when-i-try-to-select-a-file"
className="first-time-flow__link import-account__faq-link"
rel="noopener noreferrer"
target="_blank"
>
File import not working?
</a>
</div>
)
}
}
export default connect(
({ appState: { isLoading, warning } }) => ({ isLoading, warning }),
dispatch => ({
importNewAccount: (strategy, args) => dispatch(importNewAccount(strategy, args)),
hideWarning: () => dispatch(hideWarning()),
}),
)(ImportAccountScreen)

View File

@ -1,191 +0,0 @@
import {validateMnemonic} from 'bip39'
import React, { Component } from 'react'
import PropTypes from 'prop-types'
import {connect} from 'react-redux'
import {
createNewVaultAndRestore,
unMarkPasswordForgotten,
} from '../../../../ui/app/actions'
import { INITIALIZE_NOTICE_ROUTE } from '../../../../ui/app/routes'
import TextField from '../../../../ui/app/components/text-field'
class ImportSeedPhraseScreen extends Component {
static contextTypes = {
t: PropTypes.func,
}
static propTypes = {
warning: PropTypes.string,
createNewVaultAndRestore: PropTypes.func.isRequired,
leaveImportSeedScreenState: PropTypes.func,
history: PropTypes.object,
isLoading: PropTypes.bool,
};
state = {
seedPhrase: '',
password: '',
confirmPassword: '',
seedPhraseError: null,
passwordError: null,
confirmPasswordError: null,
}
parseSeedPhrase = (seedPhrase) => {
return seedPhrase
.match(/\w+/g)
.join(' ')
}
handleSeedPhraseChange (seedPhrase) {
let seedPhraseError = null
if (seedPhrase) {
const wordsCount = this.parseSeedPhrase(seedPhrase).split(' ').length
if (wordsCount !== 12 && wordsCount !== 24) {
seedPhraseError = this.context.t('seedPhraseReq')
} else if (!validateMnemonic(seedPhrase)) {
seedPhraseError = this.context.t('invalidSeedPhrase')
}
}
this.setState({ seedPhrase, seedPhraseError })
}
handlePasswordChange (password) {
const { confirmPassword } = this.state
let confirmPasswordError = null
let passwordError = null
if (password && password.length < 8) {
passwordError = this.context.t('passwordNotLongEnough')
}
if (confirmPassword && password !== confirmPassword) {
confirmPasswordError = this.context.t('passwordsDontMatch')
}
this.setState({ password, passwordError, confirmPasswordError })
}
handleConfirmPasswordChange (confirmPassword) {
const { password } = this.state
let confirmPasswordError = null
if (password !== confirmPassword) {
confirmPasswordError = this.context.t('passwordsDontMatch')
}
this.setState({ confirmPassword, confirmPasswordError })
}
onClick = () => {
const { password, seedPhrase } = this.state
const {
createNewVaultAndRestore,
leaveImportSeedScreenState,
history,
} = this.props
leaveImportSeedScreenState()
createNewVaultAndRestore(password, this.parseSeedPhrase(seedPhrase))
.then(() => history.push(INITIALIZE_NOTICE_ROUTE))
}
hasError () {
const { passwordError, confirmPasswordError, seedPhraseError } = this.state
return passwordError || confirmPasswordError || seedPhraseError
}
render () {
const {
seedPhrase,
password,
confirmPassword,
seedPhraseError,
passwordError,
confirmPasswordError,
} = this.state
const { t } = this.context
const { isLoading } = this.props
const disabled = !seedPhrase || !password || !confirmPassword || isLoading || this.hasError()
return (
<div className="first-view-main-wrapper">
<div className="first-view-main">
<div className="import-account">
<a
className="import-account__back-button"
onClick={e => {
e.preventDefault()
this.props.history.goBack()
}}
href="#"
>
{`< Back`}
</a>
<div className="import-account__title">
Import an Account with Seed Phrase
</div>
<div className="import-account__selector-label">
Enter your secret twelve word phrase here to restore your vault.
</div>
<div className="import-account__input-wrapper">
<label className="import-account__input-label">Wallet Seed</label>
<textarea
className="import-account__secret-phrase"
onChange={e => this.handleSeedPhraseChange(e.target.value)}
value={this.state.seedPhrase}
placeholder="Separate each word with a single space"
/>
</div>
<span className="error">
{ seedPhraseError }
</span>
<TextField
id="password"
label={t('newPassword')}
type="password"
className="first-time-flow__input"
value={this.state.password}
onChange={event => this.handlePasswordChange(event.target.value)}
error={passwordError}
autoComplete="new-password"
margin="normal"
largeLabel
/>
<TextField
id="confirm-password"
label={t('confirmPassword')}
type="password"
className="first-time-flow__input"
value={this.state.confirmPassword}
onChange={event => this.handleConfirmPasswordChange(event.target.value)}
error={confirmPasswordError}
autoComplete="confirm-password"
margin="normal"
largeLabel
/>
<button
className="first-time-flow__button"
onClick={() => !disabled && this.onClick()}
disabled={disabled}
>
Import
</button>
</div>
</div>
</div>
)
}
}
export default connect(
({ appState: { warning, isLoading } }) => ({ warning, isLoading }),
dispatch => ({
leaveImportSeedScreenState: () => {
dispatch(unMarkPasswordForgotten())
},
createNewVaultAndRestore: (pw, seed) => dispatch(createNewVaultAndRestore(pw, seed)),
}),
)(ImportSeedPhraseScreen)

File diff suppressed because one or more lines are too long

View File

@ -1,99 +0,0 @@
import React, { Component } from 'react'
import PropTypes from 'prop-types'
import {connect} from 'react-redux'
import { withRouter, Switch, Route } from 'react-router-dom'
import { compose } from 'recompose'
import CreatePasswordScreen from './create-password-screen'
import UniqueImageScreen from './unique-image-screen'
import NoticeScreen from './notice-screen'
import BackupPhraseScreen from './seed-screen'
import ImportAccountScreen from './import-account-screen'
import ImportSeedPhraseScreen from './import-seed-phrase-screen'
import ConfirmSeed from './confirm-seed-screen'
import {
INITIALIZE_ROUTE,
INITIALIZE_IMPORT_ACCOUNT_ROUTE,
INITIALIZE_UNIQUE_IMAGE_ROUTE,
INITIALIZE_IMPORT_WITH_SEED_PHRASE_ROUTE,
INITIALIZE_NOTICE_ROUTE,
INITIALIZE_BACKUP_PHRASE_ROUTE,
INITIALIZE_CONFIRM_SEED_ROUTE,
INITIALIZE_CREATE_PASSWORD_ROUTE,
} from '../../../../ui/app/routes'
import WelcomeScreen from '../../../../ui/app/welcome-screen'
class FirstTimeFlow extends Component {
static propTypes = {
isInitialized: PropTypes.bool,
seedWords: PropTypes.string,
address: PropTypes.string,
noActiveNotices: PropTypes.bool,
goToBuyEtherView: PropTypes.func,
isUnlocked: PropTypes.bool,
history: PropTypes.object,
welcomeScreenSeen: PropTypes.bool,
isPopup: PropTypes.bool,
};
static defaultProps = {
isInitialized: false,
seedWords: '',
noActiveNotices: false,
};
render () {
return (
<div className="flex-column flex-grow">
<div className="first-time-flow">
<Switch>
<Route exact path={INITIALIZE_IMPORT_ACCOUNT_ROUTE} component={ImportAccountScreen} />
<Route
exact
path={INITIALIZE_IMPORT_WITH_SEED_PHRASE_ROUTE}
component={ImportSeedPhraseScreen}
/>
<Route exact path={INITIALIZE_UNIQUE_IMAGE_ROUTE} component={UniqueImageScreen} />
<Route exact path={INITIALIZE_NOTICE_ROUTE} component={NoticeScreen} />
<Route exact path={INITIALIZE_BACKUP_PHRASE_ROUTE} component={BackupPhraseScreen} />
<Route exact path={INITIALIZE_CONFIRM_SEED_ROUTE} component={ConfirmSeed} />
<Route exact path={INITIALIZE_CREATE_PASSWORD_ROUTE} component={CreatePasswordScreen} />
<Route exact path={INITIALIZE_ROUTE} component={WelcomeScreen} />
</Switch>
</div>
</div>
)
}
}
const mapStateToProps = ({ metamask }) => {
const {
isInitialized,
seedWords,
noActiveNotices,
selectedAddress,
forgottenPassword,
isMascara,
isUnlocked,
welcomeScreenSeen,
isPopup,
} = metamask
return {
isMascara,
isInitialized,
seedWords,
noActiveNotices,
address: selectedAddress,
forgottenPassword,
isUnlocked,
welcomeScreenSeen,
isPopup,
}
}
export default compose(
withRouter,
connect(mapStateToProps),
)(FirstTimeFlow)

View File

@ -1,17 +0,0 @@
import React from 'react'
import PropTypes from 'prop-types'
import Spinner from './spinner'
export default function LoadingScreen ({ className = '', loadingMessage }) {
return (
<div className={`${className} loading-screen`}>
<Spinner color="#1B344D" />
<div className="loading-screen__message">{loadingMessage}</div>
</div>
)
}
LoadingScreen.propTypes = {
className: PropTypes.string,
loadingMessage: PropTypes.string,
}

View File

@ -1,135 +0,0 @@
import React, { Component } from 'react'
import PropTypes from 'prop-types'
import Markdown from 'react-markdown'
import { connect } from 'react-redux'
import { withRouter } from 'react-router-dom'
import { compose } from 'recompose'
import debounce from 'lodash.debounce'
import { markNoticeRead } from '../../../../ui/app/actions'
import Identicon from '../../../../ui/app/components/identicon'
import Breadcrumbs from './breadcrumbs'
import { INITIALIZE_BACKUP_PHRASE_ROUTE } from '../../../../ui/app/routes'
import LoadingScreen from './loading-screen'
class NoticeScreen extends Component {
static propTypes = {
address: PropTypes.string.isRequired,
nextUnreadNotice: PropTypes.shape({
title: PropTypes.string,
date: PropTypes.string,
body: PropTypes.string,
}),
location: PropTypes.shape({
state: PropTypes.shape({
next: PropTypes.func.isRequired,
}),
}),
markNoticeRead: PropTypes.func,
history: PropTypes.object,
isLoading: PropTypes.bool,
noActiveNotices: PropTypes.bool,
};
static defaultProps = {
nextUnreadNotice: {},
};
state = {
atBottom: false,
}
componentDidMount () {
if (this.props.noActiveNotices) {
this.props.history.push(INITIALIZE_BACKUP_PHRASE_ROUTE)
}
this.onScroll()
}
acceptTerms = () => {
const { markNoticeRead, nextUnreadNotice, history } = this.props
markNoticeRead(nextUnreadNotice)
.then(hasActiveNotices => {
if (!hasActiveNotices) {
history.push(INITIALIZE_BACKUP_PHRASE_ROUTE)
} else {
this.setState({ atBottom: false })
this.onScroll()
}
})
}
onScroll = debounce(() => {
if (this.state.atBottom) return
const target = document.querySelector('.tou__body')
const {scrollTop, offsetHeight, scrollHeight} = target
const atBottom = scrollTop + offsetHeight >= scrollHeight
this.setState({atBottom: atBottom})
}, 25)
render () {
const {
address,
nextUnreadNotice: { title, body },
isLoading,
} = this.props
const { atBottom } = this.state
return (
isLoading
? <LoadingScreen />
: (
<div className="first-time-flow">
<div className="first-view-main-wrapper">
<div className="first-view-main">
<div
className="tou"
onScroll={this.onScroll}
>
<Identicon address={address} diameter={70} />
<div className="tou__title">{title}</div>
<Markdown
className="tou__body markdown"
source={body}
skipHtml
/>
<button
className="first-time-flow__button"
onClick={atBottom && this.acceptTerms}
disabled={!atBottom}
>
Accept
</button>
<Breadcrumbs total={3} currentIndex={2} />
</div>
</div>
</div>
</div>
)
)
}
}
const mapStateToProps = ({ metamask, appState }) => {
const { selectedAddress, nextUnreadNotice, noActiveNotices } = metamask
const { isLoading } = appState
return {
address: selectedAddress,
nextUnreadNotice,
noActiveNotices,
isLoading,
}
}
export default compose(
withRouter,
connect(
mapStateToProps,
dispatch => ({
markNoticeRead: notice => dispatch(markNoticeRead(notice)),
}),
),
)(NoticeScreen)

View File

@ -1,177 +0,0 @@
import React, { Component } from 'react'
import PropTypes from 'prop-types'
import { connect } from 'react-redux'
import classnames from 'classnames'
import { withRouter } from 'react-router-dom'
import { compose } from 'recompose'
import Identicon from '../../../../ui/app/components/identicon'
import {exportAsFile} from '../../../../ui/app/util'
import Breadcrumbs from './breadcrumbs'
import LoadingScreen from './loading-screen'
import { DEFAULT_ROUTE, INITIALIZE_CONFIRM_SEED_ROUTE } from '../../../../ui/app/routes'
const LockIcon = props => (
<svg
version="1.1"
id="Capa_1"
xmlns="http://www.w3.org/2000/svg"
xmlnsXlink="http://www.w3.org/1999/xlink"
x="0px"
y="0px"
width="401.998px"
height="401.998px"
viewBox="0 0 401.998 401.998"
style={{enableBackground: 'new 0 0 401.998 401.998'}}
xmlSpace="preserve"
{...props}
>
<g>
<path
d="M357.45,190.721c-5.331-5.33-11.8-7.993-19.417-7.993h-9.131v-54.821c0-35.022-12.559-65.093-37.685-90.218
C266.093,12.563,236.025,0,200.998,0c-35.026,0-65.1,12.563-90.222,37.688C85.65,62.814,73.091,92.884,73.091,127.907v54.821
h-9.135c-7.611,0-14.084,2.663-19.414,7.993c-5.33,5.326-7.994,11.799-7.994,19.417V374.59c0,7.611,2.665,14.086,7.994,19.417
c5.33,5.325,11.803,7.991,19.414,7.991H338.04c7.617,0,14.085-2.663,19.417-7.991c5.325-5.331,7.994-11.806,7.994-19.417V210.135
C365.455,202.523,362.782,196.051,357.45,190.721z M274.087,182.728H127.909v-54.821c0-20.175,7.139-37.402,21.414-51.675
c14.277-14.275,31.501-21.411,51.678-21.411c20.179,0,37.399,7.135,51.677,21.411c14.271,14.272,21.409,31.5,21.409,51.675V182.728
z"
/>
</g>
</svg>
)
class BackupPhraseScreen extends Component {
static propTypes = {
isLoading: PropTypes.bool.isRequired,
address: PropTypes.string.isRequired,
seedWords: PropTypes.string,
history: PropTypes.object,
};
static defaultProps = {
seedWords: '',
}
constructor (props) {
super(props)
this.state = {
isShowingSecret: false,
}
}
// eslint-disable-next-line camelcase
UNSAFE_componentWillMount () {
const { seedWords, history } = this.props
if (!seedWords) {
history.push(DEFAULT_ROUTE)
}
}
exportSeedWords = () => {
const { seedWords } = this.props
exportAsFile('MetaMask Secret Backup Phrase', seedWords, 'text/plain')
}
renderSecretWordsContainer () {
const { isShowingSecret } = this.state
return (
<div className="backup-phrase__secret">
<div className={classnames('backup-phrase__secret-words', {
'backup-phrase__secret-words--hidden': !isShowingSecret,
})}>
{this.props.seedWords}
</div>
{!isShowingSecret && (
<div
className="backup-phrase__secret-blocker"
onClick={() => this.setState({ isShowingSecret: true })}
>
<LockIcon width="28px" height="35px" fill="#FFFFFF" />
<div
className="backup-phrase__reveal-button"
>
Click here to reveal secret words
</div>
</div>
)}
</div>
)
}
renderSecretScreen () {
const { isShowingSecret } = this.state
const { history } = this.props
return (
<div className="backup-phrase__content-wrapper">
<div className="backup-phrase__phrase">
<div className="backup-phrase__title">Secret Backup Phrase</div>
<div className="backup-phrase__body-text">
Your secret backup phrase makes it easy to back up and restore your account.
</div>
<div className="backup-phrase__body-text">
WARNING: Never disclose your backup phrase. Anyone with this phrase can take your Ether forever.
</div>
{this.renderSecretWordsContainer()}
</div>
<div className="backup-phrase__tips">
<div className="backup-phrase__tips-text">Tips:</div>
<div className="backup-phrase__tips-text">
Store this phrase in a password manager like 1Password.
</div>
<div className="backup-phrase__tips-text">
Write this phrase on a piece of paper and store in a secure location. If you want even more security, write it down on multiple pieces of paper and store each in 2 - 3 different locations.
</div>
<div className="backup-phrase__tips-text">
Memorize this phrase.
</div>
<div className="backup-phrase__tips-text">
<strong>
<a className="backup-phrase__tips-text--link backup-phrase__tips-text--strong" onClick={this.exportSeedWords}>
Download this Secret Backup Phrase
</a>
</strong> and keep it stored safely on an external encrypted hard drive or storage medium.
</div>
</div>
<div className="backup-phrase__next-button">
<button
className="first-time-flow__button"
onClick={() => isShowingSecret && history.push(INITIALIZE_CONFIRM_SEED_ROUTE)}
disabled={!isShowingSecret}
>
Next
</button>
<Breadcrumbs total={3} currentIndex={1} />
</div>
</div>
)
}
render () {
return this.props.isLoading
? <LoadingScreen loadingMessage="Creating your new account" />
: (
<div className="first-view-main-wrapper">
<div className="first-view-main">
<div className="backup-phrase">
<Identicon address={this.props.address} diameter={70} />
{this.renderSecretScreen()}
</div>
</div>
</div>
)
}
}
export default compose(
withRouter,
connect(
({ metamask: { selectedAddress, seedWords }, appState: { isLoading } }) => ({
seedWords,
isLoading,
address: selectedAddress,
}),
),
)(BackupPhraseScreen)

View File

@ -1,70 +0,0 @@
import React from 'react';
export default function Spinner({ className = '', color = "#000000" }) {
return (
<div className={`spinner ${className}`}>
<svg className="lds-spinner" width="100%" height="100%" xmlns="http://www.w3.org/2000/svg" xmlnsXlink="http://www.w3.org/1999/xlink" viewBox="0 0 100 100" preserveAspectRatio="xMidYMid" style={{background: 'none'}}>
<g transform="rotate(0 50 50)">
<rect x={47} y={16} rx={0} ry={0} width={6} height={20} fill={color}>
<animate attributeName="opacity" values="1;0" dur="1s" begin="-0.9166666666666666s" repeatCount="indefinite" />
</rect>
</g>
<g transform="rotate(30 50 50)">
<rect x={47} y={16} rx={0} ry={0} width={6} height={20} fill={color}>
<animate attributeName="opacity" values="1;0" dur="1s" begin="-0.8333333333333334s" repeatCount="indefinite" />
</rect>
</g>
<g transform="rotate(60 50 50)">
<rect x={47} y={16} rx={0} ry={0} width={6} height={20} fill={color}>
<animate attributeName="opacity" values="1;0" dur="1s" begin="-0.75s" repeatCount="indefinite" />
</rect>
</g>
<g transform="rotate(90 50 50)">
<rect x={47} y={16} rx={0} ry={0} width={6} height={20} fill={color}>
<animate attributeName="opacity" values="1;0" dur="1s" begin="-0.6666666666666666s" repeatCount="indefinite" />
</rect>
</g>
<g transform="rotate(120 50 50)">
<rect x={47} y={16} rx={0} ry={0} width={6} height={20} fill={color}>
<animate attributeName="opacity" values="1;0" dur="1s" begin="-0.5833333333333334s" repeatCount="indefinite" />
</rect>
</g>
<g transform="rotate(150 50 50)">
<rect x={47} y={16} rx={0} ry={0} width={6} height={20} fill={color}>
<animate attributeName="opacity" values="1;0" dur="1s" begin="-0.5s" repeatCount="indefinite" />
</rect>
</g>
<g transform="rotate(180 50 50)">
<rect x={47} y={16} rx={0} ry={0} width={6} height={20} fill={color}>
<animate attributeName="opacity" values="1;0" dur="1s" begin="-0.4166666666666667s" repeatCount="indefinite" />
</rect>
</g>
<g transform="rotate(210 50 50)">
<rect x={47} y={16} rx={0} ry={0} width={6} height={20} fill={color}>
<animate attributeName="opacity" values="1;0" dur="1s" begin="-0.3333333333333333s" repeatCount="indefinite" />
</rect>
</g>
<g transform="rotate(240 50 50)">
<rect x={47} y={16} rx={0} ry={0} width={6} height={20} fill={color}>
<animate attributeName="opacity" values="1;0" dur="1s" begin="-0.25s" repeatCount="indefinite" />
</rect>
</g>
<g transform="rotate(270 50 50)">
<rect x={47} y={16} rx={0} ry={0} width={6} height={20} fill={color}>
<animate attributeName="opacity" values="1;0" dur="1s" begin="-0.16666666666666666s" repeatCount="indefinite" />
</rect>
</g>
<g transform="rotate(300 50 50)">
<rect x={47} y={16} rx={0} ry={0} width={6} height={20} fill={color}>
<animate attributeName="opacity" values="1;0" dur="1s" begin="-0.08333333333333333s" repeatCount="indefinite" />
</rect>
</g>
<g transform="rotate(330 50 50)">
<rect x={47} y={16} rx={0} ry={0} width={6} height={20} fill={color}>
<animate attributeName="opacity" values="1;0" dur="1s" begin="0s" repeatCount="indefinite" />
</rect>
</g>
</svg>
</div>
);
}

View File

@ -1,50 +0,0 @@
import React, { Component } from 'react'
import PropTypes from 'prop-types'
import { withRouter } from 'react-router-dom'
import { compose } from 'recompose'
import {connect} from 'react-redux'
import Identicon from '../../../../ui/app/components/identicon'
import Breadcrumbs from './breadcrumbs'
import { INITIALIZE_NOTICE_ROUTE } from '../../../../ui/app/routes'
class UniqueImageScreen extends Component {
static propTypes = {
address: PropTypes.string,
history: PropTypes.object,
}
render () {
return (
<div className="first-view-main-wrapper">
<div className="first-view-main">
<div className="unique-image">
<Identicon address={this.props.address} diameter={70} />
<div className="unique-image__title">Your unique account image</div>
<div className="unique-image__body-text">
This image was programmatically generated for you by your new account number.
</div>
<div className="unique-image__body-text">
Youll see this image everytime you need to confirm a transaction.
</div>
<button
className="first-time-flow__button"
onClick={() => this.props.history.push(INITIALIZE_NOTICE_ROUTE)}
>
Next
</button>
<Breadcrumbs total={3} currentIndex={1} />
</div>
</div>
</div>
)
}
}
export default compose(
withRouter,
connect(
({ metamask: { selectedAddress } }) => ({
address: selectedAddress,
}),
),
)(UniqueImageScreen)

View File

@ -1,219 +0,0 @@
import React, { Component } from 'react'
import PropTypes from 'prop-types'
import classnames from 'classnames'
import {qrcode} from 'qrcode-npm'
import {connect} from 'react-redux'
import {shapeShiftSubview, pairUpdate, buyWithShapeShift} from '../../../../ui/app/actions'
import {isValidAddress} from '../../../../ui/app/util'
export class ShapeShiftForm extends Component {
static propTypes = {
selectedAddress: PropTypes.string.isRequired,
btnClass: PropTypes.string.isRequired,
tokenExchangeRates: PropTypes.object.isRequired,
coinOptions: PropTypes.object.isRequired,
shapeShiftSubview: PropTypes.func.isRequired,
pairUpdate: PropTypes.func.isRequired,
buyWithShapeShift: PropTypes.func.isRequired,
};
state = {
depositCoin: 'btc',
refundAddress: '',
showQrCode: false,
depositAddress: '',
errorMessage: '',
isLoading: false,
};
// eslint-disable-next-line camelcase
UNSAFE_componentWillMount () {
this.props.shapeShiftSubview()
}
onCoinChange = e => {
const coin = e.target.value
this.setState({
depositCoin: coin,
errorMessage: '',
})
this.props.pairUpdate(coin)
}
onBuyWithShapeShift = () => {
this.setState({
isLoading: true,
showQrCode: true,
})
const {
buyWithShapeShift,
selectedAddress: withdrawal,
} = this.props
const {
refundAddress: returnAddress,
depositCoin,
} = this.state
const pair = `${depositCoin}_eth`
const data = {
withdrawal,
pair,
returnAddress,
// Public api key
'apiKey': '803d1f5df2ed1b1476e4b9e6bcd089e34d8874595dda6a23b67d93c56ea9cc2445e98a6748b219b2b6ad654d9f075f1f1db139abfa93158c04e825db122c14b6',
}
if (isValidAddress(withdrawal)) {
buyWithShapeShift(data)
.then(d => this.setState({
showQrCode: true,
depositAddress: d.deposit,
isLoading: false,
}))
.catch(() => this.setState({
showQrCode: false,
errorMessage: 'Invalid Request',
isLoading: false,
}))
}
}
renderMetadata (label, value) {
return (
<div className="shapeshift-form__metadata-wrapper">
<div className="shapeshift-form__metadata-label">
{label}:
</div>
<div className="shapeshift-form__metadata-value">
{value}
</div>
</div>
)
}
renderMarketInfo () {
const { depositCoin } = this.state
const coinPair = `${depositCoin}_eth`
const { tokenExchangeRates } = this.props
const {
limit,
rate,
minimum,
} = tokenExchangeRates[coinPair] || {}
return (
<div className="shapeshift-form__metadata">
{this.renderMetadata('Status', limit ? 'Available' : 'Unavailable')}
{this.renderMetadata('Limit', limit)}
{this.renderMetadata('Exchange Rate', rate)}
{this.renderMetadata('Minimum', minimum)}
</div>
)
}
renderQrCode () {
const { depositAddress, isLoading } = this.state
const qrImage = qrcode(4, 'M')
qrImage.addData(depositAddress)
qrImage.make()
return (
<div className="shapeshift-form">
<div className="shapeshift-form__deposit-instruction">
Deposit your BTC to the address bellow:
</div>
<div className="shapeshift-form__qr-code">
{isLoading
? <img src="images/loading.svg" style={{ width: '60px' }} />
: <div dangerouslySetInnerHTML={{ __html: qrImage.createTableTag(4) }} />
}
</div>
{this.renderMarketInfo()}
</div>
)
}
render () {
const { coinOptions, btnClass } = this.props
const { depositCoin, errorMessage, showQrCode } = this.state
const coinPair = `${depositCoin}_eth`
const { tokenExchangeRates } = this.props
const token = tokenExchangeRates[coinPair]
return showQrCode ? this.renderQrCode() : (
<div>
<div className="shapeshift-form">
<div className="shapeshift-form__selectors">
<div className="shapeshift-form__selector">
<div className="shapeshift-form__selector-label">
Deposit
</div>
<select
className="shapeshift-form__selector-input"
value={this.state.depositCoin}
onChange={this.onCoinChange}
>
{Object.entries(coinOptions).map(([coin]) => (
<option key={coin} value={coin.toLowerCase()}>
{coin}
</option>
))}
</select>
</div>
<div
className="icon shapeshift-form__caret"
style={{ backgroundImage: 'url(images/caret-right.svg)'}}
/>
<div className="shapeshift-form__selector">
<div className="shapeshift-form__selector-label">
Receive
</div>
<div className="shapeshift-form__selector-input">
ETH
</div>
</div>
</div>
<div
className={classnames('shapeshift-form__address-input-wrapper', {
'shapeshift-form__address-input-wrapper--error': errorMessage,
})}
>
<div className="shapeshift-form__address-input-label">
Your Refund Address
</div>
<input
type="text"
className="shapeshift-form__address-input"
onChange={e => this.setState({
refundAddress: e.target.value,
errorMessage: '',
})}
/>
<div className="shapeshift-form__address-input-error-message">
{errorMessage}
</div>
</div>
{this.renderMarketInfo()}
</div>
<button
className={btnClass}
disabled={!token}
onClick={this.onBuyWithShapeShift}
>
Buy
</button>
</div>
)
}
}
export default connect(
({ metamask: { coinOptions, tokenExchangeRates, selectedAddress } }) => ({
coinOptions, tokenExchangeRates, selectedAddress,
}),
dispatch => ({
shapeShiftSubview: () => dispatch(shapeShiftSubview()),
pairUpdate: coin => dispatch(pairUpdate(coin)),
buyWithShapeShift: data => dispatch(buyWithShapeShift(data)),
}),
)(ShapeShiftForm)

View File

@ -1,134 +0,0 @@
global.window = global
const SwGlobalListener = require('sw-stream/lib/sw-global-listener.js')
const connectionListener = new SwGlobalListener(global)
const setupMultiplex = require('../../app/scripts/lib/stream-utils.js').setupMultiplex
const DbController = require('idb-global')
const SwPlatform = require('../../app/scripts/platforms/sw')
const MetamaskController = require('../../app/scripts/metamask-controller')
const Migrator = require('../../app/scripts/lib/migrator/')
const migrations = require('../../app/scripts/migrations/')
const firstTimeState = require('../../app/scripts/first-time-state')
const STORAGE_KEY = 'metamask-config'
const METAMASK_DEBUG = process.env.METAMASK_DEBUG
global.metamaskPopupIsOpen = false
const log = require('loglevel')
global.log = log
log.setDefaultLevel(METAMASK_DEBUG ? 'debug' : 'warn')
global.addEventListener('install', function (event) {
event.waitUntil(global.skipWaiting())
})
global.addEventListener('activate', function (event) {
event.waitUntil(global.clients.claim())
})
log.debug('inside:open')
// state persistence
const dbController = new DbController({
key: STORAGE_KEY,
})
start().catch(log.error)
async function start () {
log.debug('Nifty Wallet initializing...')
const initState = await loadStateFromPersistence()
await setupController(initState)
log.debug('Nifty Wallet initialization complete.')
}
//
// State and Persistence
//
async function loadStateFromPersistence () {
// migrations
const migrator = new Migrator({ migrations })
const initialState = migrator.generateInitialState(firstTimeState)
dbController.initialState = initialState
const versionedData = await dbController.open()
const migratedData = await migrator.migrateData(versionedData)
await dbController.put(migratedData)
return migratedData.data
}
async function setupController (initState, client) {
//
// MetaMask Controller
//
const platform = new SwPlatform()
const controller = new MetamaskController({
// platform specific implementation
platform,
// User confirmation callbacks:
showUnconfirmedMessage: noop,
unlockAccountMessage: noop,
showUnapprovedTx: noop,
// initial state
initState,
})
global.metamaskController = controller
controller.store.subscribe(async (state) => {
try {
const versionedData = await versionifyData(state)
await dbController.put(versionedData)
} catch (e) { console.error('METAMASK Error:', e) }
})
async function versionifyData (state) {
const rawData = await dbController.get()
return {
data: state,
meta: rawData.meta,
}
}
//
// connect to other contexts
//
connectionListener.on('remote', (portStream, messageEvent) => {
log.debug('REMOTE CONECTION FOUND***********')
connectRemote(portStream, messageEvent.data.context)
})
function connectRemote (connectionStream, context) {
var isMetaMaskInternalProcess = (context === 'popup')
if (isMetaMaskInternalProcess) {
// communication with popup
controller.setupTrustedCommunication(connectionStream, 'MetaMask')
global.metamaskPopupIsOpen = true
} else {
// communication with page
setupUntrustedCommunication(connectionStream, context)
}
}
function setupUntrustedCommunication (connectionStream, originDomain) {
// setup multiplexing
var mx = setupMultiplex(connectionStream)
// connect features
controller.setupProviderConnection(mx.createStream('provider'), originDomain)
controller.setupPublicConfig(mx.createStream('publicConfig'))
}
}
// // this will be useful later but commented out for linting for now (liiiinting)
// function sendMessageToAllClients (message) {
// global.clients.matchAll().then(function (clients) {
// clients.forEach(function (client) {
// client.postMessage(message)
// })
// })
// }
function noop () {}

View File

@ -1 +0,0 @@
global.metamask = require('metamascara')

View File

@ -1,25 +0,0 @@
const createParentStream = require('iframe-stream').ParentStream
const SwController = require('sw-controller')
const SwStream = require('sw-stream/lib/sw-stream.js')
const keepAliveDelay = Math.floor(Math.random() * (30000 - 1000)) + 1000
const background = new SwController({
fileName: './scripts/background.js',
keepAlive: true,
keepAliveInterval: 30000,
keepAliveDelay,
})
const pageStream = createParentStream()
background.on('ready', () => {
const swStream = SwStream({
serviceWorker: background.controller,
context: 'dapp',
})
pageStream.pipe(swStream).pipe(pageStream)
})
background.on('updatefound', () => window.location.reload())
background.on('error', console.error)
background.startWorker()

View File

@ -1,73 +0,0 @@
const injectCss = require('inject-css')
const SwController = require('sw-controller')
const SwStream = require('sw-stream')
const MetaMaskUiCss = require('../../old-ui/css')
const MetamascaraPlatform = require('../../app/scripts/platforms/window')
const startPopup = require('../../app/scripts/popup-core')
// create platform global
global.platform = new MetamascaraPlatform()
var css = MetaMaskUiCss()
injectCss(css)
const container = document.getElementById('app-content')
const name = 'popup'
window.METAMASK_UI_TYPE = name
window.METAMASK_PLATFORM_TYPE = 'mascara'
const keepAliveDelay = Math.floor(Math.random() * (30000 - 1000)) + 1000
const swController = new SwController({
fileName: './background.js',
keepAlive: true,
keepAliveDelay,
keepAliveInterval: 20000,
})
swController.once('updatefound', windowReload)
swController.once('ready', async () => {
try {
swController.removeListener('updatefound', windowReload)
console.log('swController ready')
await timeout(1000)
console.log('connecting to app')
await connectApp()
console.log('app connected')
} catch (err) {
console.error(err)
}
})
console.log('starting service worker')
swController.startWorker()
// Setup listener for when the service worker is read
function connectApp () {
const connectionStream = SwStream({
serviceWorker: swController.getWorker(),
context: name,
})
return new Promise((resolve, reject) => {
startPopup({ container, connectionStream }, (err, store) => {
console.log('hello from MetaMascara ui!')
if (err) reject(err)
store.subscribe(() => {
const state = store.getState()
if (state.appState.shouldClose) window.close()
})
resolve()
})
})
}
function windowReload () {
if (window.METAMASK_SKIP_RELOAD) return
window.location.reload()
}
function timeout (time) {
return new Promise((resolve) => {
setTimeout(resolve, time || 1500)
})
}

View File

@ -1,7 +0,0 @@
export default function wait (time) {
return new Promise(function (resolve, reject) {
setTimeout(function () {
resolve()
}, time * 3 || 1500)
})
}

View File

@ -1,22 +0,0 @@
var fs = require('fs')
var path = require('path')
var browserify = require('browserify')
var tests = fs.readdirSync(path.join(__dirname, 'lib'))
var bundlePath = path.join(__dirname, 'test-bundle.js')
var b = browserify()
// Remove old bundle
try {
fs.unlinkSync(bundlePath)
} catch (e) {
console.error(e)
}
var writeStream = fs.createWriteStream(bundlePath)
tests.forEach(function (fileName) {
b.add(path.join(__dirname, 'lib', fileName))
})
b.bundle().pipe(writeStream)

File diff suppressed because one or more lines are too long

View File

@ -1,10 +0,0 @@
window.addEventListener('load', () => {
window.METAMASK_SKIP_RELOAD = true
// inject app container
const body = document.body
const container = document.createElement('div')
container.id = 'app-content'
body.appendChild(container)
// start ui
require('../src/ui.js')
})

View File

@ -1,42 +0,0 @@
const EventEmitter = require('events')
const IDB = require('idb-global')
const KEY = 'metamask-test-config'
module.exports = class Helper extends EventEmitter {
tryToCleanContext () {
this.unregister()
.then(() => this.clearDb())
.then(() => super.emit('complete'))
.catch((err) => {
if (err) {
super.emit('complete')
}
})
}
unregister () {
return global.navigator.serviceWorker.getRegistration()
.then((registration) => {
if (registration) {
return registration.unregister()
.then((b) => b ? Promise.resolve() : Promise.reject())
} else return Promise.resolve()
})
}
clearDb () {
return new Promise((resolve, reject) => {
const deleteRequest = global.indexDB.deleteDatabase(KEY)
deleteRequest.addEventListener('success', resolve)
deleteRequest.addEventListener('error', reject)
})
}
mockState (state) {
const db = new IDB({
version: 2,
key: KEY,
initialState: state,
})
return db.open()
}
}

View File

@ -1,12 +0,0 @@
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<title>MetaMascara Alpha</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
</head>
<body>
<div id="app-content"></div>
<script src="./scripts/ui.js" type="text/javascript" charset="utf-8"></script>
</body>
</html>

View File

@ -6,9 +6,6 @@ const { compose } = require('recompose')
const h = require('react-hyperscript')
const actions = require('../../ui/app/actions')
const log = require('loglevel')
// mascara
const MascaraFirstTime = require('../../mascara/src/app/first-time').default
const MascaraBuyEtherScreen = require('../../mascara/src/app/first-time/buy-ether-screen').default
// init
const InitializeMenuScreen = require('./first-time/init-menu')
const NewKeyChainScreen = require('./new-keychain')
@ -81,7 +78,6 @@ function mapStateToProps (state) {
currentView: state.appState.currentView,
selectedAddress: state.metamask.selectedAddress,
transForward: state.appState.transForward,
isMascara: state.metamask.isMascara,
isOnboarding: Boolean(!noActiveNotices || seedWords || !isInitialized),
seedWords: state.metamask.seedWords,
unapprovedTxs: state.metamask.unapprovedTxs,
@ -151,24 +147,15 @@ App.prototype.render = function () {
}
App.prototype.renderLoadingIndicator = function ({ isLoading, isLoadingNetwork, loadMessage }) {
const { isMascara } = this.props
return isMascara
? null
: h(Loading, {
isLoading: isLoading || isLoadingNetwork,
loadingMessage: loadMessage,
})
return h(Loading, {
isLoading: isLoading || isLoadingNetwork,
loadingMessage: loadMessage,
})
}
App.prototype.renderPrimary = function () {
log.debug('rendering primary')
const props = this.props
const {isMascara, isOnboarding} = props
if (isMascara && isOnboarding) {
return h(MascaraFirstTime)
}
// notices
if (!props.noActiveNotices) {
@ -307,10 +294,6 @@ App.prototype.renderPrimary = function () {
log.debug('rendering buy ether screen')
return h(BuyView, {key: 'buyEthView'})
case 'onboardingBuyEth':
log.debug('rendering onboarding buy ether screen')
return h(MascaraBuyEtherScreen, {key: 'buyEthView'})
case 'qr':
log.debug('rendering show qr screen')
return h('div', {

Some files were not shown because too many files have changed in this diff Show More