commit
e283648d60
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -1,2 +1,3 @@
|
|||
; Extra environment variables
|
||||
ETH_MAINNET_RPC_ENDPOINT=00000000000
|
||||
INFURA_PROJECT_ID=00000000000
|
||||
|
|
|
@ -4,7 +4,6 @@ dist/
|
|||
docs/
|
||||
fonts/
|
||||
images/
|
||||
mascara/
|
||||
node_modules/
|
||||
notices/
|
||||
test/
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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__",
|
||||
|
|
|
@ -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)
|
||||
})
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
const ObservableStore = require('obs-store')
|
||||
import { ObservableStore } from '@metamask/obs-store'
|
||||
const extend = require('xtend')
|
||||
|
||||
class AddressBookController {
|
||||
|
|
|
@ -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'
|
||||
|
||||
|
|
|
@ -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')
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
const ObservableStore = require('obs-store')
|
||||
import { ObservableStore } from '@metamask/obs-store'
|
||||
const extend = require('xtend')
|
||||
|
||||
/**
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
const ObservableStore = require('obs-store')
|
||||
import { ObservableStore } from '@metamask/obs-store'
|
||||
const extend = require('xtend')
|
||||
const BalanceController = require('./balance')
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
const ObservableStore = require('obs-store')
|
||||
import { ObservableStore } from '@metamask/obs-store'
|
||||
const extend = require('xtend')
|
||||
const log = require('loglevel')
|
||||
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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
|
|
@ -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 })
|
||||
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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'
|
||||
|
||||
|
|
|
@ -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'
|
||||
|
||||
|
|
|
@ -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')
|
||||
|
|
|
@ -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)
|
||||
})
|
||||
|
|
|
@ -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 }) {
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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',
|
||||
]
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -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'
|
||||
|
||||
/**
|
||||
|
|
|
@ -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')
|
||||
|
|
|
@ -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')
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
const ObservableStore = require('obs-store')
|
||||
import { ObservableStore } from '@metamask/obs-store'
|
||||
const extend = require('xtend')
|
||||
const log = require('loglevel')
|
||||
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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)
|
||||
}
|
|
@ -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'
|
||||
|
||||
/**
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import ObservableStore from 'obs-store'
|
||||
import { ObservableStore } from '@metamask/obs-store'
|
||||
|
||||
/**
|
||||
* An ObservableStore that can composes a flat
|
||||
|
|
|
@ -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')
|
||||
|
||||
|
|
|
@ -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()
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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]) => {
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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,
|
||||
})
|
||||
}
|
|
@ -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()
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
}
|
|
@ -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
|
|
@ -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()
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
export { default } from './createMethodMiddleware'
|
|
@ -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'
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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')
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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>,
|
||||
|
|
|
@ -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')
|
||||
|
|
|
@ -2,7 +2,6 @@
|
|||
"metamask": {
|
||||
"isInitialized": true,
|
||||
"isUnlocked": true,
|
||||
"isMascara": false,
|
||||
"rpcTarget": "https://rawtestrpc.metamask.io/",
|
||||
"identities": {
|
||||
"0xfdea65c8e26263f6d9a1b5de9555d2931a33b825": {
|
||||
|
|
57
gulpfile.js
57
gulpfile.js
|
@ -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' ],
|
||||
},
|
||||
}))
|
||||
}
|
||||
|
|
|
@ -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
|
||||
```
|
|
@ -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')
|
||||
})
|
||||
}
|
|
@ -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>
|
|
@ -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}`)
|
|
@ -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>
|
|
@ -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
|
||||
|
||||
}
|
|
@ -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()
|
||||
})
|
||||
}
|
||||
|
||||
}
|
|
@ -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 world’s 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)
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
}
|
|
@ -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 world’s 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)
|
|
@ -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)
|
|
@ -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)
|
|
@ -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)
|
|
@ -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
|
@ -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)
|
|
@ -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,
|
||||
}
|
|
@ -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)
|
|
@ -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)
|
|
@ -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>
|
||||
);
|
||||
}
|
|
@ -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">
|
||||
You’ll 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)
|
|
@ -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)
|
|
@ -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 () {}
|
|
@ -1 +0,0 @@
|
|||
global.metamask = require('metamascara')
|
|
@ -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()
|
|
@ -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)
|
||||
})
|
||||
}
|
|
@ -1,7 +0,0 @@
|
|||
export default function wait (time) {
|
||||
return new Promise(function (resolve, reject) {
|
||||
setTimeout(function () {
|
||||
resolve()
|
||||
}, time * 3 || 1500)
|
||||
})
|
||||
}
|
|
@ -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
|
@ -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')
|
||||
})
|
|
@ -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()
|
||||
}
|
||||
}
|
|
@ -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>
|
|
@ -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
Loading…
Reference in New Issue