1
0
mirror of https://github.com/kremalicious/metamask-extension.git synced 2024-11-22 01:47:00 +01:00

Merge branch 'develop' into testing

This commit is contained in:
Thomas 2018-05-07 14:30:43 -07:00
commit 8250fd9d6d
114 changed files with 7531 additions and 1710 deletions

View File

@ -15,19 +15,13 @@ workflows:
- test-lint:
requires:
- prep-deps-npm
- test-deps:
requires:
- prep-deps-npm
- test-e2e:
requires:
- prep-deps-npm
- prep-build
- job-screens:
requires:
- prep-deps-npm
- prep-build
- job-publish:
requires:
- prep-deps-npm
- prep-build
- job-screens
- test-unit:
requires:
- prep-deps-npm
@ -52,13 +46,24 @@ workflows:
- all-tests-pass:
requires:
- test-lint
- test-deps
- test-unit
- test-e2e
- job-screens
- test-integration-mascara-chrome
- test-integration-mascara-firefox
- test-integration-flat-chrome
- test-integration-flat-firefox
- job-screens:
requires:
- prep-deps-npm
- prep-build
- all-tests-pass
- job-publish:
requires:
- prep-deps-npm
- prep-build
- job-screens
- all-tests-pass
jobs:
prep-deps-npm:
@ -144,6 +149,17 @@ jobs:
name: Test
command: npm run lint
test-deps:
docker:
- image: circleci/node:8-browsers
steps:
- checkout
- restore_cache:
key: dependency-cache-{{ .Revision }}
- run:
name: Test
command: npx nsp check
test-e2e:
docker:
- image: circleci/node:8-browsers

View File

@ -2,11 +2,14 @@
## Current Master
## 4.6.0 Thu Apr 26 2018
- Correctly format currency conversion for locally selected preferred currency.
- Improved performance of 3D fox logo.
- Fetch token prices based on contract address, not symbol
- Fix bug that prevents setting language locale in settings.
- Show checksum addresses throughout the UI
- Allow transactions with a 0 gwei gas price
## 4.5.5 Fri Apr 06 2018

View File

@ -12,12 +12,14 @@ For any new programmatic functionality, we like unit tests when possible, so if
### PR Format
We use [waffle](https://waffle.io/) for project management, and it will automatically keep us organized if you do one simple thing:
If this PR closes the issue, add the line `Fixes #$ISSUE_NUMBER`. Ex. For closing issue 418, include the line `Fixes #418`.
If it doesn't close the issue but addresses it partially, just include a reference to the issue number, like `#418`.
Submit your PR against the `develop` branch. This is where we merge new features so they get some time to receive extra testing before being pushed to `master` for production.
If your PR is a hot-fix that needs to be published urgently, you may submit a PR against the `master` branch, but this PR will receive tighter scrutiny before merging.
## Before Merging
Make sure you get a `:thumbsup`, `:+1`, or `LGTM` from another collaborator before merging.

View File

@ -98,6 +98,9 @@
"clickCopy": {
"message": "Click to Copy"
},
"close": {
"message": "Close"
},
"confirm": {
"message": "Confirm"
},
@ -259,6 +262,9 @@
"enterPasswordConfirm": {
"message": "Enter your password to confirm"
},
"enterPasswordContinue": {
"message": "Enter password to continue"
},
"passwordNotLongEnough": {
"message": "Password not long enough"
},
@ -331,6 +337,9 @@
"gasPriceRequired": {
"message": "Gas Price Required"
},
"generatingTransaction": {
"message": "Generating transaction"
},
"getEther": {
"message": "Get Ether"
},
@ -476,6 +485,9 @@
"metamaskDescription": {
"message": "MetaMask is a secure identity vault for Ethereum."
},
"metamaskSeedWords": {
"message": "MetaMask Seed Words"
},
"min": {
"message": "Minimum"
},
@ -549,6 +561,9 @@
"message": "or",
"description": "choice between creating or importing a new account"
},
"password": {
"message": "Password"
},
"passwordCorrect": {
"message": "Please make sure your password is correct."
},
@ -634,8 +649,17 @@
"revealSeedWords": {
"message": "Reveal Seed Words"
},
"revealSeedWordsTitle": {
"message": "Seed Phrase"
},
"revealSeedWordsDescription": {
"message": "If you ever change browsers or move computers, you will need this seed phrase to access your accounts. Save them somewhere safe and secret."
},
"revealSeedWordsWarningTitle": {
"message": "DO NOT share this phrase with anyone!"
},
"revealSeedWordsWarning": {
"message": "Do not recover your seed words in a public place! These words can be used to steal all your accounts."
"message": "These words can be used to steal all your accounts."
},
"revert": {
"message": "Revert"
@ -677,6 +701,9 @@
"reprice_subtitle": {
"message": "Increase your gas price to attempt to overwrite and speed up your transaction"
},
"saveAsCsvFile": {
"message": "Save as CSV File"
},
"saveAsFile": {
"message": "Save as File",
"description": "Account export process"
@ -909,7 +936,7 @@
"youSign": {
"message": "You are signing"
},
"generatingTransaction": {
"message": "Generating transaction"
"yourPrivateSeedPhrase": {
"message": "Your private seed phrase"
}
}

View File

@ -181,7 +181,7 @@
"message": "DEN je vaša šifrirana shramba v MetaMasku."
},
"deposit": {
"message": "Vplačilo"
"message": "Vplačaj"
},
"depositBTC": {
"message": "Vplačajte vaš BTC na spodnji naslov:"
@ -507,10 +507,10 @@
"message": "Ni se začelo"
},
"oldUI": {
"message": "Starejši uporabniški vmesnik"
"message": "Star UI"
},
"oldUIMessage": {
"message": "Vrnili ste se v starejši uporabniški vmesnik. V novega se lahko vrnete z možnostjo v spustnem meniju v zgornjem desnem kotu."
"message": "Vrnili ste se v star uporabniški vmesnik. V novega se lahko vrnete z možnostjo v spustnem meniju v zgornjem desnem kotu."
},
"or": {
"message": "ali",
@ -759,7 +759,7 @@
"message": "Vpišite vaše geslo"
},
"uiWelcome": {
"message": "Dobrodošli v novem uporabniškem vmesniku (Beta)"
"message": "Dobrodošli v nov UI (Beta)"
},
"uiWelcomeMessage": {
"message": "Zdaj uporabljate novi MetaMask uporabniški vmesnik. Razglejte se, preizkusite nove funkcije, kot so pošiljanje žetonov, in nas obvestite, če imate kakšne težave."

View File

@ -0,0 +1,24 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="18px" height="17px" viewBox="0 0 18 17" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<!-- Generator: sketchtool 49.3 (51167) - http://www.bohemiancoding.com/sketch -->
<title>374E58A5-C29E-4921-83E7-889FA06D6408</title>
<desc>Created with sketchtool.</desc>
<defs></defs>
<g id="Reveal-Seedphrase" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="Seed-phrase-2" transform="translate(-39.000000, -379.000000)">
<g id="Group-2">
<g id="Group-8" transform="translate(16.000000, 248.000000)">
<g id="Group-6" transform="translate(23.336478, 120.000000)">
<g id="Group-5" transform="translate(0.408805, 11.000000)">
<g id="copy-to-clipboard">
<rect id="Rectangle-18" stroke="#3098DC" stroke-width="2" x="1" y="1" width="12.0220126" height="12"></rect>
<rect id="Rectangle-18-Copy-2" fill="#FFFFFF" x="2.1572327" y="2" width="14.0220126" height="14"></rect>
<rect id="Rectangle-18-Copy" stroke="#3098DC" stroke-width="2" x="4.23584906" y="4" width="12.0220126" height="12"></rect>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@ -1,15 +1,26 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 16.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
width="24.088px" height="24px" viewBox="138.01 0 24.088 24" enable-background="new 138.01 0 24.088 24" xml:space="preserve" fill="#F7861C">
<g>
<polygon fill="#F7861C" points="157.551,17.075 156.55,17.075 156.55,19.149 142.569,19.149 142.569,17.075 141.568,17.075
141.568,20.145 141.955,20.145 141.955,20.15 157.006,20.15 157.006,20.145 157.551,20.145 "/>
<polygon fill="#F7861C" points="152.555,10.275 152.555,11.26 152.555,11.268 151.562,11.268 151.562,12.252 150.565,12.252
150.565,4.171 149.564,4.171 149.564,12.236 148.564,12.236 148.564,11.252 147.564,11.252 147.564,11.236 147.564,11.221
147.564,10.236 146.563,10.236 146.563,11.221 146.563,11.236 146.563,12.221 147.563,12.221 147.563,12.236 147.563,12.252
147.563,13.236 148.563,13.236 148.563,14.221 149.564,14.221 149.564,15.725 150.565,15.725 150.565,14.236 151.563,14.236
151.563,13.252 152.563,13.252 152.563,12.268 152.563,12.26 153.556,12.26 153.556,11.275 153.556,11.26 153.556,10.275 "/>
</g>
</svg>
<?xml version="1.0" encoding="UTF-8"?>
<svg width="20px" height="18px" viewBox="0 0 20 18" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<!-- Generator: sketchtool 49.3 (51167) - http://www.bohemiancoding.com/sketch -->
<title>50559280-0739-419A-8E87-3CDD16A6996A</title>
<desc>Created with sketchtool.</desc>
<defs></defs>
<g id="Reveal-Seedphrase" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="Seed-phrase-2" transform="translate(-212.000000, -379.000000)" stroke="#259DE5" stroke-width="2">
<g id="Group-2">
<g id="Group-8" transform="translate(16.000000, 248.000000)">
<g id="Group-6" transform="translate(23.336478, 120.000000)">
<g id="Group-3" transform="translate(174.000000, 11.000000)">
<g id="Group-4">
<g id="download">
<polyline id="Path-5" points="0 11 0 17 17 17 17 11"></polyline>
<path d="M8.5,0 L8.5,11" id="Path-6"></path>
<polyline id="Path-7" points="3.1875 7 8.5 11 13.8125 7"></polyline>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

22
app/images/warning.svg Normal file
View File

@ -0,0 +1,22 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="33px" height="32px" viewBox="0 0 33 32" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<!-- Generator: Sketch 49.3 (51167) - http://www.bohemiancoding.com/sketch -->
<title>Group 7</title>
<desc>Created with Sketch.</desc>
<defs></defs>
<g id="Reveal-Seedphrase" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="Seed-phrase-2" transform="translate(-29.000000, -155.000000)">
<g id="Group-2" transform="translate(0.000000, 132.000000)">
<g id="Group" transform="translate(28.000000, 19.000000)">
<g id="Group-19-Copy-2" transform="translate(0.000000, 3.000000)">
<g id="Group-7">
<path d="M20.1321134,3.85444772 L32.5721829,26.6020033 C33.367162,28.0556794 32.8331826,29.8785746 31.3795065,30.6735537 C30.9381289,30.9149321 30.4431378,31.0414403 29.9400695,31.0414403 L5.05993054,31.0414403 C3.40307629,31.0414403 2.05993054,29.6982946 2.05993054,28.0414403 C2.05993054,27.538372 2.18643873,27.0433809 2.42781712,26.6020033 L14.8678866,3.85444772 C15.6628657,2.40077162 17.4857609,1.86679221 18.939437,2.66177133 C19.442875,2.93708896 19.8567958,3.35100977 20.1321134,3.85444772 Z" id="Triangle-2-Copy" stroke="#FF001F" stroke-width="2"></path>
<rect id="Rectangle-5" fill="#FF001F" x="16" y="9" width="3" height="13"></rect>
<rect id="Rectangle-5-Copy" fill="#FF001F" x="16" y="24" width="3" height="3"></rect>
</g>
</g>
</g>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.7 KiB

View File

@ -1,7 +1,7 @@
{
"name": "__MSG_appName__",
"short_name": "__MSG_appName__",
"version": "4.5.5",
"version": "4.6.0",
"manifest_version": 2,
"author": "https://metamask.io",
"description": "__MSG_appDescription__",

View File

@ -1,3 +1,7 @@
/**
* @file The entry point for the web extension singleton process.
*/
const urlUtil = require('url')
const endOfStream = require('end-of-stream')
const pump = require('pump')
@ -61,6 +65,90 @@ initialize().catch(log.error)
// setup metamask mesh testing container
setupMetamaskMeshMetrics()
/**
* An object representing a transaction, in whatever state it is in.
* @typedef TransactionMeta
*
* @property {number} id - An internally unique tx identifier.
* @property {number} time - Time the tx was first suggested, in unix epoch time (ms).
* @property {string} status - The current transaction status (unapproved, signed, submitted, dropped, failed, rejected), as defined in `tx-state-manager.js`.
* @property {string} metamaskNetworkId - The transaction's network ID, used for EIP-155 compliance.
* @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.
* @property {string} hash - A hex string of the transaction hash, used to identify the transaction on the network.
* @property {number} submittedTime - The time the transaction was submitted to the network, in Unix epoch time (ms).
*/
/**
* The data emitted from the MetaMaskController.store EventEmitter, also used to initialize the MetaMaskController. Available in UI on React state as state.metamask.
* @typedef MetaMaskState
* @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 {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.
* @property {string} provider.rpcTarget - The address for the RPC API, if using an RPC API.
* @property {string} provider.type - An identifier for the type of network selected, allows MetaMask to use custom provider strategies for known networks.
* @property {string} network - A stringified number of the current network ID.
* @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 {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 {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 {number} unapprovedTypedMsgCount - The number of messages in unapprovedTypedMsgs.
* @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 {Array} lostAccounts - TODO: Remove this feature. A leftover from the version-3 migration where our seed-phrase library changed to fix a bug where some accounts were mis-generated, but we recovered the old accounts as "lost" instead of losing them.
* @property {boolean} forgottenPassword - Returns true if the user has initiated the password recovery screen, is recovering from seed phrase.
*/
/**
* @typedef VersionedData
* @property {MetaMaskState} data - The data emitted from MetaMask controller, or used to initialize it.
* @property {Number} version - The latest migration version that has been run.
*/
/**
* Initializes the MetaMask controller, and sets up all platform configuration.
* @returns {Promise} Setup complete.
*/
async function initialize () {
const initState = await loadStateFromPersistence()
const initLangCode = await getFirstPreferredLangCode()
@ -72,6 +160,11 @@ async function initialize () {
// State and Persistence
//
/**
* Loads any stored data, prioritizing the latest storage strategy.
* Migrates that data schema in case it was last loaded on an older version.
* @returns {Promise<MetaMaskState>} Last data emitted from previous instance of MetaMask.
*/
async function loadStateFromPersistence () {
// migrations
const migrator = new Migrator({ migrations })
@ -134,6 +227,16 @@ async function loadStateFromPersistence () {
return versionedData.data
}
/**
* Initializes the MetaMask Controller with any initial state and default language.
* Configures platform-specific error reporting strategy.
* Streams emitted state updates to platform-specific storage strategy.
* Creates platform listeners for new Dapps/Contexts, and sets up their data connections to the controller.
*
* @param {Object} initState - The initial state to start the controller with, matches the state that is emitted from the controller.
* @param {String} initLangCode - The region code for the language preferred by the current user.
* @returns {Promise} After setup is complete.
*/
function setupController (initState, initLangCode) {
//
// MetaMask Controller
@ -158,7 +261,11 @@ function setupController (initState, initLangCode) {
controller.txController.on(`tx:status-update`, (txId, status) => {
if (status !== 'failed') return
const txMeta = controller.txController.txStateManager.getTx(txId)
reportFailedTxToSentry({ raven, txMeta })
try {
reportFailedTxToSentry({ raven, txMeta })
} catch (e) {
console.error(e)
}
})
// setup state persistence
@ -172,6 +279,11 @@ function setupController (initState, initLangCode) {
}
)
/**
* Assigns the given state to the versioned object (with metadata), and returns that.
* @param {Object} state - The state object as emitted by the MetaMaskController.
* @returns {VersionedData} The state object wrapped in an object that includes a metadata key.
*/
function versionifyData (state) {
versionedData.data = state
return versionedData
@ -208,6 +320,18 @@ function setupController (initState, initLangCode) {
return popupIsOpen || Boolean(Object.keys(openMetamaskTabsIDs).length) || notificationIsOpen
}
/**
* A runtime.Port object, as provided by the browser:
* @link https://developer.mozilla.org/en-US/Add-ons/WebExtensions/API/runtime/Port
* @typedef Port
* @type Object
*/
/**
* Connects a Port to the MetaMask controller via a multiplexed duplex stream.
* This method identifies trusted (MetaMask) interfaces, and connects them differently from untrusted (web pages).
* @param {Port} remotePort - The port provided by a new context.
*/
function connectRemote (remotePort) {
const processName = remotePort.name
const isMetaMaskInternalProcess = metamaskInternalProcessHash[processName]
@ -261,7 +385,10 @@ function setupController (initState, initLangCode) {
controller.messageManager.on('updateBadge', updateBadge)
controller.personalMessageManager.on('updateBadge', updateBadge)
// plugin badge text
/**
* Updates the Web Extension's "badge" number, on the little fox in the toolbar.
* The number reflects the current number of pending transactions or message signatures needing user approval.
*/
function updateBadge () {
var label = ''
var unapprovedTxCount = controller.txController.getUnapprovedTxCount()
@ -283,7 +410,9 @@ function setupController (initState, initLangCode) {
// Etc...
//
// popup trigger
/**
* Opens the browser popup for user confirmation
*/
function triggerUi () {
extension.tabs.query({ active: true }, tabs => {
const currentlyActiveMetamaskTab = Boolean(tabs.find(tab => openMetamaskTabsIDs[tab.id]))

View File

@ -1,79 +0,0 @@
const MAINET_RPC_URL = 'https://mainnet.infura.io/metamask'
const ROPSTEN_RPC_URL = 'https://ropsten.infura.io/metamask'
const KOVAN_RPC_URL = 'https://kovan.infura.io/metamask'
const RINKEBY_RPC_URL = 'https://rinkeby.infura.io/metamask'
const LOCALHOST_RPC_URL = 'http://localhost:8545'
const MAINET_RPC_URL_BETA = 'https://mainnet.infura.io/metamask2'
const ROPSTEN_RPC_URL_BETA = 'https://ropsten.infura.io/metamask2'
const KOVAN_RPC_URL_BETA = 'https://kovan.infura.io/metamask2'
const RINKEBY_RPC_URL_BETA = 'https://rinkeby.infura.io/metamask2'
const DEFAULT_RPC = 'rinkeby'
const OLD_UI_NETWORK_TYPE = 'network'
const BETA_UI_NETWORK_TYPE = 'networkBeta'
global.METAMASK_DEBUG = process.env.METAMASK_DEBUG
/**
* @typedef {Object} UrlConfig
* @property {string} localhost URL of local RPC provider
* @property {string} mainnet URL of mainnet RPC provider
* @property {string} ropsten URL of Ropsten testnet RPC provider
* @property {string} kovan URL of Kovan testnet RPC provider
* @property {string} rinkeby URL of Rinkeby testnet RPC provider
*/
/**
* @typedef {Object} NameConfig
* @property {string} 3 URL of local RPC provider
* @property {string} 4 URL of mainnet RPC provider
* @property {string} 42 URL of Ropsten testnet RPC provider
*/
/**
* @typedef {Object} EnumConfig
* @property {string} DEFAULT_RPC Default network provider URL
* @property {string} OLD_UI_NETWORK_TYPE Network associated with old UI
* @property {string} BETA_UI_NETWORK_TYPE Network associated with new UI
*/
/**
* @typedef {Object} Config
* @property {UrlConfig} network Network configuration parameters
* @property {UrlConfig} networkBeta Beta UI network configuration parameters
* @property {NameConfig} networkNames Network name configuration parameters
* @property {EnumConfig} enums Application-wide string constants
*/
/**
* @type {Config}
**/
const config = {
network: {
localhost: LOCALHOST_RPC_URL,
mainnet: MAINET_RPC_URL,
ropsten: ROPSTEN_RPC_URL,
kovan: KOVAN_RPC_URL,
rinkeby: RINKEBY_RPC_URL,
},
networkBeta: {
localhost: LOCALHOST_RPC_URL,
mainnet: MAINET_RPC_URL_BETA,
ropsten: ROPSTEN_RPC_URL_BETA,
kovan: KOVAN_RPC_URL_BETA,
rinkeby: RINKEBY_RPC_URL_BETA,
},
networkNames: {
3: 'Ropsten',
4: 'Rinkeby',
42: 'Kovan',
},
enums: {
DEFAULT_RPC,
OLD_UI_NETWORK_TYPE,
BETA_UI_NETWORK_TYPE,
},
}
module.exports = config

View File

@ -174,6 +174,7 @@ function blacklistedDomainCheck () {
'uscourts.gov',
'dropbox.com',
'webbyawards.com',
'cdn.shopify.com/s/javascripts/tricorder/xtld-read-only-frame.html',
]
var currentUrl = window.location.href
var currentRegex

View File

@ -4,6 +4,24 @@ const BN = require('ethereumjs-util').BN
class BalanceController {
/**
* Controller responsible for storing and updating an account's balance.
*
* @typedef {Object} BalanceController
* @param {Object} opts Initialize various properties of the class.
* @property {string} address A base 16 hex string. The account address which has the balance managed by this
* BalanceController.
* @property {AccountTracker} accountTracker Stores and updates the users accounts
* for which this BalanceController manages balance.
* @property {TransactionController} txController Stores, tracks and manages transactions. Here used to create a listener for
* transaction updates.
* @property {BlockTracker} blockTracker Tracks updates to blocks. On new blocks, this BalanceController updates its balance
* @property {Object} store The store for the ethBalance
* @property {string} store.ethBalance A base 16 hex string. The balance for the current account.
* @property {PendingBalanceCalculator} balanceCalc Used to calculate the accounts balance with possible pending
* transaction costs taken into account.
*
*/
constructor (opts = {}) {
this._validateParams(opts)
const { address, accountTracker, txController, blockTracker } = opts
@ -26,6 +44,11 @@ class BalanceController {
this._registerUpdates()
}
/**
* Updates the ethBalance property to the current pending balance
*
* @returns {Promise<void>} Promises undefined
*/
async updateBalance () {
const balance = await this.balanceCalc.getBalance()
this.store.updateState({
@ -33,6 +56,15 @@ class BalanceController {
})
}
/**
* Sets up listeners and subscriptions which should trigger an update of ethBalance. These updates include:
* - when a transaction changes state to 'submitted', 'confirmed' or 'failed'
* - when the current account changes (i.e. a new account is selected)
* - when there is a block update
*
* @private
*
*/
_registerUpdates () {
const update = this.updateBalance.bind(this)
@ -51,6 +83,14 @@ class BalanceController {
this.blockTracker.on('block', update)
}
/**
* Gets the balance, as a base 16 hex string, of the account at this BalanceController's current address.
* If the current account has no balance, returns undefined.
*
* @returns {Promise<BN|void>} Promises a BN with a value equal to the balance of the current account, or undefined
* if the current account has no balance
*
*/
async _getBalance () {
const { accounts } = this.accountTracker.store.getState()
const entry = accounts[this.address]
@ -58,6 +98,14 @@ class BalanceController {
return balance ? new BN(balance.substring(2), 16) : undefined
}
/**
* Gets the pending transactions (i.e. those with a 'submitted' status). These are accessed from the
* TransactionController passed to this BalanceController during construction.
*
* @private
* @returns {Promise<array>} Promises an array of transaction objects.
*
*/
async _getPendingTransactions () {
const pending = this.txController.getFilteredTxList({
from: this.address,
@ -67,6 +115,14 @@ class BalanceController {
return pending
}
/**
* Validates that the passed options have all required properties.
*
* @param {Object} opts The options object to validate
* @throws {string} Throw a custom error indicating that address, accountTracker, txController and blockTracker are
* missing and at least one is required
*
*/
_validateParams (opts) {
const { address, accountTracker, txController, blockTracker } = opts
if (!address || !accountTracker || !txController || !blockTracker) {

View File

@ -10,6 +10,22 @@ const POLLING_INTERVAL = 4 * 60 * 1000
class BlacklistController {
/**
* Responsible for polling for and storing an up to date 'eth-phishing-detect' config.json file, while
* exposing a method that can check whether a given url is a phishing attempt. The 'eth-phishing-detect'
* config.json file contains a fuzzylist, whitelist and blacklist.
*
*
* @typedef {Object} BlacklistController
* @param {object} opts Overrides the defaults for the initial state of this.store
* @property {object} store The the store of the current phishing config
* @property {object} store.phishing Contains fuzzylist, whitelist and blacklist arrays. @see
* {@link https://github.com/MetaMask/eth-phishing-detect/blob/master/src/config.json}
* @property {object} _phishingDetector The PhishingDetector instantiated by passing store.phishing to
* PhishingDetector.
* @property {object} _phishingUpdateIntervalRef Id of the interval created to periodically update the blacklist
*
*/
constructor (opts = {}) {
const initState = extend({
phishing: PHISHING_DETECTION_CONFIG,
@ -22,16 +38,28 @@ class BlacklistController {
this._phishingUpdateIntervalRef = null
}
//
// PUBLIC METHODS
//
/**
* Given a url, returns the result of checking if that url is in the store.phishing blacklist
*
* @param {string} hostname The hostname portion of a url; the one that will be checked against the white and
* blacklists of store.phishing
* @returns {boolean} Whether or not the passed hostname is on our phishing blacklist
*
*/
checkForPhishing (hostname) {
if (!hostname) return false
const { result } = this._phishingDetector.check(hostname)
return result
}
/**
* Queries `https://api.infura.io/v2/blacklist` for an updated blacklist config. This is passed to this._phishingDetector
* to update our phishing detector instance, and is updated in the store. The new phishing config is returned
*
*
* @returns {Promise<object>} Promises the updated blacklist config for the phishingDetector
*
*/
async updatePhishingList () {
const response = await fetch('https://api.infura.io/v2/blacklist')
const phishing = await response.json()
@ -40,6 +68,11 @@ class BlacklistController {
return phishing
}
/**
* Initiates the updating of the local blacklist at a set interval. The update is done via this.updatePhishingList().
* Also, this method store a reference to that interval at this._phishingUpdateIntervalRef
*
*/
scheduleUpdates () {
if (this._phishingUpdateIntervalRef) return
this.updatePhishingList().catch(log.warn)
@ -48,10 +81,14 @@ class BlacklistController {
}, POLLING_INTERVAL)
}
//
// PRIVATE METHODS
//
/**
* Sets this._phishingDetector to a new PhishingDetector instance.
* @see {@link https://github.com/MetaMask/eth-phishing-detect}
*
* @private
* @param {object} config A config object like that found at {@link https://github.com/MetaMask/eth-phishing-detect/blob/master/src/config.json}
*
*/
_setupPhishingDetector (config) {
this._phishingDetector = new PhishingDetector(config)
}

View File

@ -0,0 +1,56 @@
const ROPSTEN = 'ropsten'
const RINKEBY = 'rinkeby'
const KOVAN = 'kovan'
const MAINNET = 'mainnet'
const LOCALHOST = 'localhost'
const ROPSTEN_CODE = 3
const RINKEYBY_CODE = 4
const KOVAN_CODE = 42
const ROPSTEN_DISPLAY_NAME = 'Ropsten'
const RINKEBY_DISPLAY_NAME = 'Rinkeby'
const KOVAN_DISPLAY_NAME = 'Kovan'
const MAINNET_DISPLAY_NAME = 'Main Ethereum Network'
const MAINNET_RPC_URL = 'https://mainnet.infura.io/metamask'
const ROPSTEN_RPC_URL = 'https://ropsten.infura.io/metamask'
const KOVAN_RPC_URL = 'https://kovan.infura.io/metamask'
const RINKEBY_RPC_URL = 'https://rinkeby.infura.io/metamask'
const LOCALHOST_RPC_URL = 'http://localhost:8545'
const MAINNET_RPC_URL_BETA = 'https://mainnet.infura.io/metamask2'
const ROPSTEN_RPC_URL_BETA = 'https://ropsten.infura.io/metamask2'
const KOVAN_RPC_URL_BETA = 'https://kovan.infura.io/metamask2'
const RINKEBY_RPC_URL_BETA = 'https://rinkeby.infura.io/metamask2'
const DEFAULT_NETWORK = 'rinkeby'
const OLD_UI_NETWORK_TYPE = 'network'
const BETA_UI_NETWORK_TYPE = 'networkBeta'
module.exports = {
ROPSTEN,
RINKEBY,
KOVAN,
MAINNET,
LOCALHOST,
ROPSTEN_CODE,
RINKEYBY_CODE,
KOVAN_CODE,
ROPSTEN_DISPLAY_NAME,
RINKEBY_DISPLAY_NAME,
KOVAN_DISPLAY_NAME,
MAINNET_DISPLAY_NAME,
MAINNET_RPC_URL,
ROPSTEN_RPC_URL,
KOVAN_RPC_URL,
RINKEBY_RPC_URL,
LOCALHOST_RPC_URL,
MAINNET_RPC_URL_BETA,
ROPSTEN_RPC_URL_BETA,
KOVAN_RPC_URL_BETA,
RINKEBY_RPC_URL_BETA,
DEFAULT_NETWORK,
OLD_UI_NETWORK_TYPE,
BETA_UI_NETWORK_TYPE,
}

View File

@ -0,0 +1,2 @@
const NetworkController = require('./network')
module.exports = NetworkController

View File

@ -1,17 +1,24 @@
const assert = require('assert')
const EventEmitter = require('events')
const createMetamaskProvider = require('web3-provider-engine/zero.js')
const SubproviderFromProvider = require('web3-provider-engine/subproviders/web3.js')
const SubproviderFromProvider = require('web3-provider-engine/subproviders/provider.js')
const createInfuraProvider = require('eth-json-rpc-infura/src/createProvider')
const ObservableStore = require('obs-store')
const ComposedStore = require('obs-store/lib/composed')
const extend = require('xtend')
const EthQuery = require('eth-query')
const createEventEmitterProxy = require('../lib/events-proxy.js')
const networkConfig = require('../config.js')
const createEventEmitterProxy = require('../../lib/events-proxy.js')
const log = require('loglevel')
const { OLD_UI_NETWORK_TYPE, DEFAULT_RPC } = networkConfig.enums
const INFURA_PROVIDER_TYPES = ['ropsten', 'rinkeby', 'kovan', 'mainnet']
const {
ROPSTEN,
RINKEBY,
KOVAN,
MAINNET,
OLD_UI_NETWORK_TYPE,
DEFAULT_NETWORK,
} = require('./enums')
const { getNetworkEndpoints } = require('./util')
const INFURA_PROVIDER_TYPES = [ROPSTEN, RINKEBY, KOVAN, MAINNET]
module.exports = class NetworkController extends EventEmitter {
@ -19,8 +26,8 @@ module.exports = class NetworkController extends EventEmitter {
super()
this._networkEndpointVersion = OLD_UI_NETWORK_TYPE
this._networkEndpoints = this.getNetworkEndpoints(OLD_UI_NETWORK_TYPE)
this._defaultRpc = this._networkEndpoints[DEFAULT_RPC]
this._networkEndpoints = getNetworkEndpoints(OLD_UI_NETWORK_TYPE)
this._defaultRpc = this._networkEndpoints[DEFAULT_NETWORK]
config.provider.rpcTarget = this.getRpcAddressForType(config.provider.type, config.provider)
this.networkStore = new ObservableStore('loading')
@ -37,17 +44,13 @@ module.exports = class NetworkController extends EventEmitter {
}
this._networkEndpointVersion = version
this._networkEndpoints = this.getNetworkEndpoints(version)
this._defaultRpc = this._networkEndpoints[DEFAULT_RPC]
this._networkEndpoints = getNetworkEndpoints(version)
this._defaultRpc = this._networkEndpoints[DEFAULT_NETWORK]
const { type } = this.getProviderConfig()
return this.setProviderType(type, true)
}
getNetworkEndpoints (version = OLD_UI_NETWORK_TYPE) {
return networkConfig[version]
}
initializeProvider (_providerParams) {
this._baseProviderParams = _providerParams
const { type, rpcTarget } = this.providerStore.getState()

View File

@ -0,0 +1,65 @@
const {
ROPSTEN,
RINKEBY,
KOVAN,
MAINNET,
LOCALHOST,
ROPSTEN_CODE,
RINKEYBY_CODE,
KOVAN_CODE,
ROPSTEN_DISPLAY_NAME,
RINKEBY_DISPLAY_NAME,
KOVAN_DISPLAY_NAME,
MAINNET_DISPLAY_NAME,
MAINNET_RPC_URL,
ROPSTEN_RPC_URL,
KOVAN_RPC_URL,
RINKEBY_RPC_URL,
LOCALHOST_RPC_URL,
MAINNET_RPC_URL_BETA,
ROPSTEN_RPC_URL_BETA,
KOVAN_RPC_URL_BETA,
RINKEBY_RPC_URL_BETA,
OLD_UI_NETWORK_TYPE,
BETA_UI_NETWORK_TYPE,
} = require('./enums')
const networkToNameMap = {
[ROPSTEN]: ROPSTEN_DISPLAY_NAME,
[RINKEBY]: RINKEBY_DISPLAY_NAME,
[KOVAN]: KOVAN_DISPLAY_NAME,
[MAINNET]: MAINNET_DISPLAY_NAME,
[ROPSTEN_CODE]: ROPSTEN_DISPLAY_NAME,
[RINKEYBY_CODE]: RINKEBY_DISPLAY_NAME,
[KOVAN_CODE]: KOVAN_DISPLAY_NAME,
}
const networkEndpointsMap = {
[OLD_UI_NETWORK_TYPE]: {
[LOCALHOST]: LOCALHOST_RPC_URL,
[MAINNET]: MAINNET_RPC_URL,
[ROPSTEN]: ROPSTEN_RPC_URL,
[KOVAN]: KOVAN_RPC_URL,
[RINKEBY]: RINKEBY_RPC_URL,
},
[BETA_UI_NETWORK_TYPE]: {
[LOCALHOST]: LOCALHOST_RPC_URL,
[MAINNET]: MAINNET_RPC_URL_BETA,
[ROPSTEN]: ROPSTEN_RPC_URL_BETA,
[KOVAN]: KOVAN_RPC_URL_BETA,
[RINKEBY]: RINKEBY_RPC_URL_BETA,
},
}
const getNetworkDisplayName = key => networkToNameMap[key]
const getNetworkEndpoints = (networkType = OLD_UI_NETWORK_TYPE) => {
return {
...networkEndpointsMap[networkType],
}
}
module.exports = {
getNetworkDisplayName,
getNetworkEndpoints,
}

View File

@ -8,8 +8,8 @@ class PreferencesController {
*
* @typedef {Object} PreferencesController
* @param {object} opts Overrides the defaults for the initial state of this.store
* @property {object} store The an object containing a users preferences, stored in local storage
* @property {array} store.frequentRpcList A list of custom rpcs to provide the user
* @property {object} store The stored object containing a users preferences, stored in local storage
* @property {array} store.frequentRpcList A list of custom rpcs to provide the user
* @property {string} store.currentAccountTab Indicates the selected tab in the ui
* @property {array} store.tokens The tokens the user wants display in their token lists
* @property {boolean} store.useBlockie The users preference for blockie identicons within the UI

View File

@ -6,6 +6,23 @@ const log = require('loglevel')
class RecentBlocksController {
/**
* Controller responsible for storing, updating and managing the recent history of blocks. Blocks are back filled
* upon the controller's construction and then the list is updated when the given block tracker gets a 'block' event
* (indicating that there is a new block to process).
*
* @typedef {Object} RecentBlocksController
* @param {object} opts Contains objects necessary for tracking blocks and querying the blockchain
* @param {BlockTracker} opts.blockTracker Contains objects necessary for tracking blocks and querying the blockchain
* @param {BlockTracker} opts.provider The provider used to create a new EthQuery instance.
* @property {BlockTracker} blockTracker Points to the passed BlockTracker. On RecentBlocksController construction,
* listens for 'block' events so that new blocks can be processed and added to storage.
* @property {EthQuery} ethQuery Points to the EthQuery instance created with the passed provider
* @property {number} historyLength The maximum length of blocks to track
* @property {object} store Stores the recentBlocks
* @property {array} store.recentBlocks Contains all recent blocks, up to a total that is equal to this.historyLength
*
*/
constructor (opts = {}) {
const { blockTracker, provider } = opts
this.blockTracker = blockTracker
@ -21,12 +38,23 @@ class RecentBlocksController {
this.backfill()
}
/**
* Sets store.recentBlocks to an empty array
*
*/
resetState () {
this.store.updateState({
recentBlocks: [],
})
}
/**
* Receives a new block and modifies it with this.mapTransactionsToPrices. Then adds that block to the recentBlocks
* array in storage. If the recentBlocks array contains the maximum number of blocks, the oldest block is removed.
*
* @param {object} newBlock The new block to modify and add to the recentBlocks array
*
*/
processBlock (newBlock) {
const block = this.mapTransactionsToPrices(newBlock)
@ -40,6 +68,15 @@ class RecentBlocksController {
this.store.updateState(state)
}
/**
* Receives a new block and modifies it with this.mapTransactionsToPrices. Adds that block to the recentBlocks
* array in storage, but only if the recentBlocks array contains fewer than the maximum permitted.
*
* Unlike this.processBlock, backfillBlock adds the modified new block to the beginning of the recent block array.
*
* @param {object} newBlock The new block to modify and add to the beginning of the recentBlocks array
*
*/
backfillBlock (newBlock) {
const block = this.mapTransactionsToPrices(newBlock)
@ -52,6 +89,14 @@ class RecentBlocksController {
this.store.updateState(state)
}
/**
* Receives a block and gets the gasPrice of each of its transactions. These gas prices are added to the block at a
* new property, and the block's transactions are removed.
*
* @param {object} newBlock The block to modify. It's transaction array will be replaced by a gasPrices array.
* @returns {object} The modified block.
*
*/
mapTransactionsToPrices (newBlock) {
const block = extend(newBlock, {
gasPrices: newBlock.transactions.map((tx) => {
@ -62,6 +107,16 @@ class RecentBlocksController {
return block
}
/**
* On this.blockTracker's first 'block' event after this RecentBlocksController's instantiation, the store.recentBlocks
* array is populated with this.historyLength number of blocks. The block number of the this.blockTracker's first
* 'block' event is used to iteratively generate all the numbers of the previous blocks, which are obtained by querying
* the blockchain. These blocks are backfilled so that the recentBlocks array is ordered from oldest to newest.
*
* Each iteration over the block numbers is delayed by 100 milliseconds.
*
* @returns {Promise<void>} Promises undefined
*/
async backfill() {
this.blockTracker.once('block', async (block) => {
let blockNum = block.number
@ -90,12 +145,25 @@ class RecentBlocksController {
})
}
/**
* A helper for this.backfill. Provides an easy way to ensure a 100 millisecond delay using await
*
* @returns {Promise<void>} Promises undefined
*
*/
async wait () {
return new Promise((resolve) => {
setTimeout(resolve, 100)
})
}
/**
* Uses EthQuery to get a block that has a given block number.
*
* @param {number} number The number of the block to get
* @returns {Promise<object>} Promises A block with the passed number
*
*/
async getBlockByNumber (number) {
const bn = new BN(number)
return new Promise((resolve, reject) => {

View File

@ -1,4 +1,5 @@
const ObservableStore = require('obs-store')
const { warn } = require('loglevel')
// By default, poll every 3 minutes
const DEFAULT_INTERVAL = 180 * 1000
@ -39,10 +40,13 @@ class TokenRatesController {
*/
async fetchExchangeRate (address) {
try {
const response = await fetch(`https://exchanges.balanc3.net/prices?from=${address}&to=ETH&autoConversion=false&summaryOnly=true`)
const response = await fetch(`https://metamask.balanc3.net/prices?from=${address}&to=ETH&autoConversion=false&summaryOnly=true`)
const json = await response.json()
return json && json.length ? json[0].averagePrice : 0
} catch (error) { }
} catch (error) {
warn(`MetaMask - TokenRatesController exchange rate fetch failed for ${address}.`, error)
return 0
}
}
/**

View File

@ -0,0 +1,92 @@
# Transaction Controller
Transaction Controller is an aggregate of sub-controllers and trackers
exposed to the MetaMask controller.
- txStateManager
responsible for the state of a transaction and
storing the transaction
- pendingTxTracker
watching blocks for transactions to be include
and emitting confirmed events
- txGasUtil
gas calculations and safety buffering
- nonceTracker
calculating nonces
## Flow diagram of processing a transaction
![transaction-flow](../../../../docs/transaction-flow.png)
## txMeta's & txParams
A txMeta is the "meta" object it has all the random bits of info we need about a transaction on it. txParams are sacred every thing on txParams gets signed so it must
be a valid key and be hex prefixed except for the network number. Extra stuff must go on the txMeta!
Here is a txMeta too look at:
```js
txMeta = {
"id": 2828415030114568, // unique id for this txMeta used for look ups
"time": 1524094064821, // time of creation
"status": "confirmed",
"metamaskNetworkId": "1524091532133", //the network id for the transaction
"loadingDefaults": false, // used to tell the ui when we are done calculatyig gass defaults
"txParams": { // the txParams object
"from": "0x8acce2391c0d510a6c5e5d8f819a678f79b7e675",
"to": "0x8acce2391c0d510a6c5e5d8f819a678f79b7e675",
"value": "0x0",
"gasPrice": "0x3b9aca00",
"gas": "0x7b0c",
"nonce": "0x0"
},
"history": [{ //debug
"id": 2828415030114568,
"time": 1524094064821,
"status": "unapproved",
"metamaskNetworkId": "1524091532133",
"loadingDefaults": true,
"txParams": {
"from": "0x8acce2391c0d510a6c5e5d8f819a678f79b7e675",
"to": "0x8acce2391c0d510a6c5e5d8f819a678f79b7e675",
"value": "0x0"
}
},
[
{
"op": "add",
"path": "/txParams/gasPrice",
"value": "0x3b9aca00"
},
...], // I've removed most of history for this
"gasPriceSpecified": false, //whether or not the user/dapp has specified gasPrice
"gasLimitSpecified": false, //whether or not the user/dapp has specified gas
"estimatedGas": "5208",
"origin": "MetaMask", //debug
"nonceDetails": {
"params": {
"highestLocallyConfirmed": 0,
"highestSuggested": 0,
"nextNetworkNonce": 0
},
"local": {
"name": "local",
"nonce": 0,
"details": {
"startPoint": 0,
"highest": 0
}
},
"network": {
"name": "network",
"nonce": 0,
"details": {
"baseCount": 0
}
}
},
"rawTx": "0xf86980843b9aca00827b0c948acce2391c0d510a6c5e5d8f819a678f79b7e67580808602c5b5de66eea05c01a320b96ac730cb210ca56d2cb71fa360e1fc2c21fa5cf333687d18eb323fa02ed05987a6e5fd0f2459fcff80710b76b83b296454ad9a37594a0ccb4643ea90", // used for rebroadcast
"hash": "0xa45ba834b97c15e6ff4ed09badd04ecd5ce884b455eb60192cdc73bcc583972a",
"submittedTime": 1524094077902 // time of the attempt to submit the raw tx to the network, used in the ui to show the retry button
}
```

View File

@ -3,28 +3,42 @@ const ObservableStore = require('obs-store')
const ethUtil = require('ethereumjs-util')
const Transaction = require('ethereumjs-tx')
const EthQuery = require('ethjs-query')
const TransactionStateManager = require('../lib/tx-state-manager')
const TxGasUtil = require('../lib/tx-gas-utils')
const PendingTransactionTracker = require('../lib/pending-tx-tracker')
const NonceTracker = require('../lib/nonce-tracker')
const TransactionStateManager = require('./tx-state-manager')
const TxGasUtil = require('./tx-gas-utils')
const PendingTransactionTracker = require('./pending-tx-tracker')
const NonceTracker = require('./nonce-tracker')
const txUtils = require('./lib/util')
const log = require('loglevel')
/*
/**
Transaction Controller is an aggregate of sub-controllers and trackers
composing them in a way to be exposed to the metamask controller
- txStateManager
<br>- txStateManager
responsible for the state of a transaction and
storing the transaction
- pendingTxTracker
<br>- pendingTxTracker
watching blocks for transactions to be include
and emitting confirmed events
- txGasUtil
<br>- txGasUtil
gas calculations and safety buffering
- nonceTracker
<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
*/
module.exports = class TransactionController extends EventEmitter {
class TransactionController extends EventEmitter {
constructor (opts) {
super()
this.networkStore = opts.networkStore || new ObservableStore({})
@ -38,45 +52,19 @@ module.exports = class TransactionController extends EventEmitter {
this.query = new EthQuery(this.provider)
this.txGasUtil = new TxGasUtil(this.provider)
this._mapMethods()
this.txStateManager = new TransactionStateManager({
initState: opts.initState,
txHistoryLimit: opts.txHistoryLimit,
getNetwork: this.getNetwork.bind(this),
})
this.txStateManager.getFilteredTxList({
status: 'unapproved',
loadingDefaults: true,
}).forEach((tx) => {
this.addTxDefaults(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)
})
})
this.txStateManager.getFilteredTxList({
status: 'approved',
}).forEach((txMeta) => {
const txSignError = new Error('Transaction found as "approved" during boot - possibly stuck during signing')
this.txStateManager.setTxStatusFailed(txMeta.id, txSignError)
})
this._onBootCleanUp()
this.store = this.txStateManager.store
this.txStateManager.on('tx:status-update', this.emit.bind(this, 'tx:status-update'))
this.nonceTracker = new NonceTracker({
provider: this.provider,
getPendingTransactions: this.txStateManager.getPendingTransactions.bind(this.txStateManager),
getConfirmedTransactions: (address) => {
return this.txStateManager.getFilteredTxList({
from: address,
status: 'confirmed',
err: undefined,
})
},
getConfirmedTransactions: this.txStateManager.getConfirmedTransactions.bind(this.txStateManager),
})
this.pendingTxTracker = new PendingTransactionTracker({
@ -88,60 +76,14 @@ module.exports = class TransactionController extends EventEmitter {
})
this.txStateManager.store.subscribe(() => this.emit('update:badge'))
this.pendingTxTracker.on('tx:warning', (txMeta) => {
this.txStateManager.updateTx(txMeta, 'transactions/pending-tx-tracker#event: tx:warning')
})
this.pendingTxTracker.on('tx:confirmed', (txId) => this._markNonceDuplicatesDropped(txId))
this.pendingTxTracker.on('tx:failed', this.txStateManager.setTxStatusFailed.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++
this.txStateManager.updateTx(txMeta, 'transactions/pending-tx-tracker#event: tx:retry')
})
this.blockTracker.on('block', this.pendingTxTracker.checkForTxInBlock.bind(this.pendingTxTracker))
// this is a little messy but until ethstore has been either
// removed or redone this is to guard against the race condition
this.blockTracker.on('latest', this.pendingTxTracker.resubmitPendingTxs.bind(this.pendingTxTracker))
this.blockTracker.on('sync', this.pendingTxTracker.queryPendingTxs.bind(this.pendingTxTracker))
this._setupListners()
// memstore is computed from a few different stores
this._updateMemstore()
this.txStateManager.store.subscribe(() => this._updateMemstore())
this.networkStore.subscribe(() => this._updateMemstore())
this.preferencesStore.subscribe(() => this._updateMemstore())
}
getState () {
return this.memStore.getState()
}
getNetwork () {
return this.networkStore.getState()
}
getSelectedAddress () {
return this.preferencesStore.getState().selectedAddress
}
getUnapprovedTxCount () {
return Object.keys(this.txStateManager.getUnapprovedTxList()).length
}
getPendingTxCount (account) {
return this.txStateManager.getPendingTransactions(account).length
}
getFilteredTxList (opts) {
return this.txStateManager.getFilteredTxList(opts)
}
/** @returns {number} the chainId*/
getChainId () {
const networkState = this.networkStore.getState()
const getChainId = parseInt(networkState)
@ -152,16 +94,45 @@ module.exports = class TransactionController extends EventEmitter {
}
}
wipeTransactions (address) {
this.txStateManager.wipeTransactions(address)
}
// Adds a tx to the txlist
/**
Adds a tx to the txlist
@emits ${txMeta.id}:unapproved
*/
addTx (txMeta) {
this.txStateManager.addTx(txMeta)
this.emit(`${txMeta.id}:unapproved`, txMeta)
}
/**
Wipes the transactions for a given account
@param {string} address - hex string of the from address for txs being removed
*/
wipeTransactions (address) {
this.txStateManager.wipeTransactions(address)
}
/**
Check if a txMeta in the list with the same nonce has been confirmed in a block
if the txParams dont have a nonce will return false
@returns {boolean} weather the nonce has been used in a transaction confirmed in a block
@param {object} txMeta - the txMeta object
*/
async isNonceTaken (txMeta) {
const { from, nonce } = txMeta.txParams
if ('nonce' in txMeta.txParams) {
const sameNonceTxList = this.txStateManager.getFilteredTxList({from, nonce, status: 'confirmed'})
return (sameNonceTxList.length >= 1)
}
return false
}
/**
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
*/
async newUnapprovedTransaction (txParams, opts = {}) {
log.debug(`MetaMaskController newUnapprovedTransaction ${JSON.stringify(txParams)}`)
const initialTxMeta = await this.addUnapprovedTransaction(txParams)
@ -184,17 +155,24 @@ module.exports = class TransactionController extends EventEmitter {
})
}
/**
Validates and generates a txMeta with defaults and puts it in txStateManager
store
@returns {txMeta}
*/
async addUnapprovedTransaction (txParams) {
// validate
const normalizedTxParams = this._normalizeTxParams(txParams)
this._validateTxParams(normalizedTxParams)
const normalizedTxParams = txUtils.normalizeTxParams(txParams)
txUtils.validateTxParams(normalizedTxParams)
// construct txMeta
let txMeta = this.txStateManager.generateTxMeta({ txParams: normalizedTxParams })
this.addTx(txMeta)
this.emit('newUnapprovedTx', txMeta)
// add default tx params
try {
txMeta = await this.addTxDefaults(txMeta)
txMeta = await this.addTxGasDefaults(txMeta)
} catch (error) {
console.log(error)
this.txStateManager.setTxStatusFailed(txMeta.id, error)
@ -206,21 +184,33 @@ module.exports = class TransactionController extends EventEmitter {
return txMeta
}
async addTxDefaults (txMeta) {
/**
adds the tx gas defaults: gas && gasPrice
@param txMeta {Object} - the txMeta object
@returns {Promise<object>} resolves with txMeta
*/
async addTxGasDefaults (txMeta) {
const txParams = txMeta.txParams
// ensure value
txParams.value = txParams.value ? ethUtil.addHexPrefix(txParams.value) : '0x0'
txMeta.gasPriceSpecified = Boolean(txParams.gasPrice)
let gasPrice = txParams.gasPrice
if (!gasPrice) {
gasPrice = this.getGasPrice ? this.getGasPrice() : await this.query.gasPrice()
}
txParams.gasPrice = ethUtil.addHexPrefix(gasPrice.toString(16))
txParams.value = txParams.value || '0x0'
// set gasLimit
return await this.txGasUtil.analyzeGasUsage(txMeta)
}
/**
Creates a new txMeta with the same txParams as the original
to allow the user to resign the transaction with a higher gas values
@param originalTxId {number} - the id of the txMeta that
you want to attempt to retry
@return {txMeta}
*/
async retryTransaction (originalTxId) {
const originalTxMeta = this.txStateManager.getTx(originalTxId)
const lastGasPrice = originalTxMeta.txParams.gasPrice
@ -234,15 +224,31 @@ module.exports = class TransactionController extends EventEmitter {
return txMeta
}
/**
updates the txMeta in the txStateManager
@param txMeta {Object} - the updated txMeta
*/
async updateTransaction (txMeta) {
this.txStateManager.updateTx(txMeta, 'confTx: user updated transaction')
}
/**
updates and approves the transaction
@param txMeta {Object}
*/
async updateAndApproveTransaction (txMeta) {
this.txStateManager.updateTx(txMeta, 'confTx: user approved transaction')
await this.approveTransaction(txMeta.id)
}
/**
sets the tx status to approved
auto fills the nonce
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
*/
async approveTransaction (txId) {
let nonceLock
try {
@ -274,7 +280,11 @@ module.exports = 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}
*/
async signTransaction (txId) {
const txMeta = this.txStateManager.getTx(txId)
// add network/chain id
@ -290,6 +300,12 @@ module.exports = class TransactionController extends EventEmitter {
return rawTx
}
/**
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
@returns {Promise<void>}
*/
async publishTransaction (txId, rawTx) {
const txMeta = this.txStateManager.getTx(txId)
txMeta.rawTx = rawTx
@ -299,11 +315,20 @@ module.exports = class TransactionController extends EventEmitter {
this.txStateManager.setTxStatusSubmitted(txId)
}
/**
Convenience method for the ui thats sets the transaction to rejected
@param txId {number} - the tx's Id
@returns {Promise<void>}
*/
async cancelTransaction (txId) {
this.txStateManager.setTxStatusRejected(txId)
}
// receives a txHash records the tx as signed
/**
Sets the txHas on the txMeta
@param txId {number} - the tx's Id
@param txHash {string} - the hash for the txMeta
*/
setTxHash (txId, txHash) {
// Add the tx hash to the persisted meta-tx object
const txMeta = this.txStateManager.getTx(txId)
@ -314,63 +339,92 @@ module.exports = class TransactionController extends EventEmitter {
//
// PRIVATE METHODS
//
/** maps methods for convenience*/
_mapMethods () {
/** @returns the state in transaction controller */
this.getState = () => this.memStore.getState()
/** @returns the network number stored in networkStore */
this.getNetwork = () => this.networkStore.getState()
/** @returns the user selected address */
this.getSelectedAddress = () => this.preferencesStore.getState().selectedAddress
/** Returns an array of transactions whos status is unapproved */
this.getUnapprovedTxCount = () => Object.keys(this.txStateManager.getUnapprovedTxList()).length
/**
@returns a number that represents how many transactions have the status submitted
@param account {String} - hex prefixed account
*/
this.getPendingTxCount = (account) => this.txStateManager.getPendingTransactions(account).length
/** see txStateManager */
this.getFilteredTxList = (opts) => this.txStateManager.getFilteredTxList(opts)
}
_normalizeTxParams (txParams) {
// functions that handle normalizing of that key in txParams
const whiteList = {
from: from => ethUtil.addHexPrefix(from).toLowerCase(),
to: to => ethUtil.addHexPrefix(txParams.to).toLowerCase(),
nonce: nonce => ethUtil.addHexPrefix(nonce),
value: value => ethUtil.addHexPrefix(value),
data: data => ethUtil.addHexPrefix(data),
gas: gas => ethUtil.addHexPrefix(gas),
gasPrice: gasPrice => ethUtil.addHexPrefix(gasPrice),
}
/**
If transaction controller was rebooted with transactions that are uncompleted
in steps of the transaction signing or user confirmation process it will either
transition txMetas to a failed state or try to redo those tasks.
*/
// apply only keys in the whiteList
const normalizedTxParams = {}
Object.keys(whiteList).forEach((key) => {
if (txParams[key]) normalizedTxParams[key] = whiteList[key](txParams[key])
_onBootCleanUp () {
this.txStateManager.getFilteredTxList({
status: 'unapproved',
loadingDefaults: true,
}).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)
})
})
return normalizedTxParams
this.txStateManager.getFilteredTxList({
status: 'approved',
}).forEach((txMeta) => {
const txSignError = new Error('Transaction found as "approved" during boot - possibly stuck during signing')
this.txStateManager.setTxStatusFailed(txMeta.id, txSignError)
})
}
_validateTxParams (txParams) {
this._validateFrom(txParams)
this._validateRecipient(txParams)
if ('value' in txParams) {
const value = txParams.value.toString()
if (value.includes('-')) {
throw new Error(`Invalid transaction value of ${txParams.value} not a positive number.`)
/**
is called in constructor applies the listeners for pendingTxTracker txStateManager
and blockTracker
*/
_setupListners () {
this.txStateManager.on('tx:status-update', this.emit.bind(this, 'tx:status-update'))
this.pendingTxTracker.on('tx:warning', (txMeta) => {
this.txStateManager.updateTx(txMeta, 'transactions/pending-tx-tracker#event: tx:warning')
})
this.pendingTxTracker.on('tx:confirmed', (txId) => this.txStateManager.setTxStatusConfirmed(txId))
this.pendingTxTracker.on('tx:confirmed', (txId) => this._markNonceDuplicatesDropped(txId))
this.pendingTxTracker.on('tx:failed', this.txStateManager.setTxStatusFailed.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++
this.txStateManager.updateTx(txMeta, 'transactions/pending-tx-tracker#event: tx:retry')
})
this.blockTracker.on('block', this.pendingTxTracker.checkForTxInBlock.bind(this.pendingTxTracker))
// this is a little messy but until ethstore has been either
// removed or redone this is to guard against the race condition
this.blockTracker.on('latest', this.pendingTxTracker.resubmitPendingTxs.bind(this.pendingTxTracker))
this.blockTracker.on('sync', this.pendingTxTracker.queryPendingTxs.bind(this.pendingTxTracker))
if (value.includes('.')) {
throw new Error(`Invalid transaction value of ${txParams.value} number must be in wei`)
}
}
}
_validateFrom (txParams) {
if ( !(typeof txParams.from === 'string') ) throw new Error(`Invalid from address ${txParams.from} not a string`)
if (!ethUtil.isValidAddress(txParams.from)) throw new Error('Invalid from address')
}
_validateRecipient (txParams) {
if (txParams.to === '0x' || txParams.to === null ) {
if (txParams.data) {
delete txParams.to
} else {
throw new Error('Invalid recipient address')
}
} else if ( txParams.to !== undefined && !ethUtil.isValidAddress(txParams.to) ) {
throw new Error('Invalid recipient address')
}
return txParams
}
/**
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
*/
_markNonceDuplicatesDropped (txId) {
this.txStateManager.setTxStatusConfirmed(txId)
// get the confirmed transactions nonce and from address
const txMeta = this.txStateManager.getTx(txId)
const { nonce, from } = txMeta.txParams
@ -385,6 +439,9 @@ module.exports = class TransactionController extends EventEmitter {
})
}
/**
Updates the memStore in transaction controller
*/
_updateMemstore () {
const unapprovedTxs = this.txStateManager.getUnapprovedTxList()
const selectedAddressTxList = this.txStateManager.getFilteredTxList({
@ -394,3 +451,5 @@ module.exports = class TransactionController extends EventEmitter {
this.memStore.updateState({ unapprovedTxs, selectedAddressTxList })
}
}
module.exports = TransactionController

View File

@ -1,6 +1,6 @@
const jsonDiffer = require('fast-json-patch')
const clone = require('clone')
/** @module*/
module.exports = {
generateHistoryEntry,
replayHistory,
@ -8,7 +8,11 @@ module.exports = {
migrateFromSnapshotsToDiffs,
}
/**
converts non-initial history entries into diffs
@param longHistory {array}
@returns {array}
*/
function migrateFromSnapshotsToDiffs (longHistory) {
return (
longHistory
@ -20,6 +24,17 @@ function migrateFromSnapshotsToDiffs (longHistory) {
)
}
/**
generates an array of history objects sense the previous state.
The object has the keys opp(the operation preformed),
path(the key and if a nested object then each key will be seperated with a `/`)
value
with the first entry having the note
@param previousState {object} - the previous state of the object
@param newState {object} - the update object
@param note {string} - a optional note for the state change
@reurns {array}
*/
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
@ -27,11 +42,19 @@ function generateHistoryEntry (previousState, newState, note) {
return entry
}
/**
Recovers previous txMeta state obj
@return {object}
*/
function replayHistory (_shortHistory) {
const shortHistory = clone(_shortHistory)
return shortHistory.reduce((val, entry) => jsonDiffer.applyPatch(val, entry).newDocument)
}
/**
@param txMeta {Object}
@returns {object} a clone object of the txMeta with out history
*/
function snapshotFromTxMeta (txMeta) {
// create txMeta snapshot for history
const snapshot = clone(txMeta)

View File

@ -0,0 +1,99 @@
const {
addHexPrefix,
isValidAddress,
} = require('ethereumjs-util')
/**
@module
*/
module.exports = {
normalizeTxParams,
validateTxParams,
validateFrom,
validateRecipient,
getFinalStates,
}
// functions that handle normalizing of that key in txParams
const normalizers = {
from: from => addHexPrefix(from).toLowerCase(),
to: to => addHexPrefix(to).toLowerCase(),
nonce: nonce => addHexPrefix(nonce),
value: value => addHexPrefix(value),
data: data => addHexPrefix(data),
gas: gas => addHexPrefix(gas),
gasPrice: gasPrice => addHexPrefix(gasPrice),
}
/**
normalizes txParams
@param txParams {object}
@returns {object} normalized txParams
*/
function normalizeTxParams (txParams) {
// apply only keys in the normalizers
const normalizedTxParams = {}
for (const key in normalizers) {
if (txParams[key]) normalizedTxParams[key] = normalizers[key](txParams[key])
}
return normalizedTxParams
}
/**
validates txParams
@param txParams {object}
*/
function validateTxParams (txParams) {
validateFrom(txParams)
validateRecipient(txParams)
if ('value' in txParams) {
const value = txParams.value.toString()
if (value.includes('-')) {
throw new Error(`Invalid transaction value of ${txParams.value} not a positive number.`)
}
if (value.includes('.')) {
throw new Error(`Invalid transaction value of ${txParams.value} number must be in wei`)
}
}
}
/**
validates the from field in txParams
@param txParams {object}
*/
function validateFrom (txParams) {
if (!(typeof txParams.from === 'string')) throw new Error(`Invalid from address ${txParams.from} not a string`)
if (!isValidAddress(txParams.from)) throw new Error('Invalid from address')
}
/**
validates the to field in txParams
@param txParams {object}
*/
function validateRecipient (txParams) {
if (txParams.to === '0x' || txParams.to === null) {
if (txParams.data) {
delete txParams.to
} else {
throw new Error('Invalid recipient address')
}
} else if (txParams.to !== undefined && !isValidAddress(txParams.to)) {
throw new Error('Invalid recipient address')
}
return txParams
}
/**
@returns an {array} of states that can be considered final
*/
function getFinalStates () {
return [
'rejected', // the user has responded no!
'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
]
}

View File

@ -1,7 +1,15 @@
const EthQuery = require('ethjs-query')
const assert = require('assert')
const Mutex = require('await-semaphore').Mutex
/**
@param opts {Object}
@param {Object} opts.provider a ethereum provider
@param {Function} opts.getPendingTransactions a function that returns an array of txMeta
whosee status is `submitted`
@param {Function} opts.getConfirmedTransactions a function that returns an array of txMeta
whose status is `confirmed`
@class
*/
class NonceTracker {
constructor ({ provider, getPendingTransactions, getConfirmedTransactions }) {
@ -12,6 +20,9 @@ class NonceTracker {
this.lockMap = {}
}
/**
@returns {Promise<Object>} with the key releaseLock (the gloabl mutex)
*/
async getGlobalLock () {
const globalMutex = this._lookupMutex('global')
// await global mutex free
@ -19,8 +30,20 @@ class NonceTracker {
return { releaseLock }
}
// releaseLock must be called
// releaseLock must be called after adding signed tx to pending transactions (or discarding)
/**
* @typedef NonceDetails
* @property {number} highestLocallyConfirmed - A hex string of the highest nonce on a confirmed transaction.
* @property {number} nextNetworkNonce - The next nonce suggested by the eth_getTransactionCount method.
* @property {number} highetSuggested - The maximum between the other two, the number returned.
*/
/**
this will return an object with the `nextNonce` `nonceDetails` of type NonceDetails, and the releaseLock
Note: releaseLock must be called after adding a signed tx to pending transactions (or discarding).
@param address {string} the hex string for the address whose nonce we are calculating
@returns {Promise<NonceDetails>}
*/
async getNonceLock (address) {
// await global mutex free
await this._globalMutexFree()
@ -123,6 +146,17 @@ class NonceTracker {
return highestNonce
}
/**
@typedef {object} highestContinuousFrom
@property {string} - name the name for how the nonce was calculated based on the data used
@property {number} - nonce the next suggested nonce
@property {object} - details the provided starting nonce that was used (for debugging)
*/
/**
@param txList {array} - list of txMeta's
@param startPoint {number} - the highest known locally confirmed nonce
@returns {highestContinuousFrom}
*/
_getHighestContinuousFrom (txList, startPoint) {
const nonces = txList.map((txMeta) => {
const nonce = txMeta.txParams.nonce
@ -140,6 +174,10 @@ class NonceTracker {
// this is a hotfix for the fact that the blockTracker will
// change when the network changes
/**
@returns {Object} the current blockTracker
*/
_getBlockTracker () {
return this.provider._blockTracker
}

View File

@ -1,23 +1,24 @@
const EventEmitter = require('events')
const log = require('loglevel')
const EthQuery = require('ethjs-query')
/*
Utility class for tracking the transactions as they
go from a pending state to a confirmed (mined in a block) state
/**
Event emitter utility class for tracking the transactions as they<br>
go from a pending state to a confirmed (mined in a block) state<br>
<br>
As well as continues broadcast while in the pending state
<br>
@param config {object} - non optional configuration object consists of:
@param {Object} config.provider - A network provider.
@param {Object} config.nonceTracker see nonce tracker
@param {function} config.getPendingTransactions a function for getting an array of transactions,
@param {function} config.publishTransaction a async function for publishing raw transactions,
~config is not optional~
requires a: {
provider: //,
nonceTracker: //see nonce tracker,
getPendingTransactions: //() a function for getting an array of transactions,
publishTransaction: //(rawTx) a async function for publishing raw transactions,
}
@class
*/
module.exports = class PendingTransactionTracker extends EventEmitter {
class PendingTransactionTracker extends EventEmitter {
constructor (config) {
super()
this.query = new EthQuery(config.provider)
@ -29,8 +30,13 @@ module.exports = class PendingTransactionTracker extends EventEmitter {
this._checkPendingTxs()
}
// checks if a signed tx is in a block and
// if included sets the tx status as 'confirmed'
/**
checks if a signed tx is in a block and
if it is included emits tx status as 'confirmed'
@param block {object}, a full block
@emits tx:confirmed
@emits tx:failed
*/
checkForTxInBlock (block) {
const signedTxList = this.getPendingTransactions()
if (!signedTxList.length) return
@ -52,6 +58,11 @@ module.exports = class PendingTransactionTracker extends EventEmitter {
})
}
/**
asks the network for the transaction to see if a block number is included on it
if we have skipped/missed blocks
@param object - oldBlock newBlock
*/
queryPendingTxs ({ oldBlock, newBlock }) {
// check pending transactions on start
if (!oldBlock) {
@ -63,7 +74,11 @@ module.exports = class PendingTransactionTracker extends EventEmitter {
if (diff > 1) this._checkPendingTxs()
}
/**
Will resubmit any transactions who have not been confirmed in a block
@param block {object} - a block object
@emits tx:warning
*/
resubmitPendingTxs (block) {
const pending = this.getPendingTransactions()
// only try resubmitting if their are transactions to resubmit
@ -100,6 +115,13 @@ module.exports = class PendingTransactionTracker extends EventEmitter {
}))
}
/**
resubmits the individual txMeta used in resubmitPendingTxs
@param txMeta {Object} - txMeta object
@param latestBlockNumber {string} - hex string for the latest block number
@emits tx:retry
@returns txHash {string}
*/
async _resubmitTx (txMeta, latestBlockNumber) {
if (!txMeta.firstRetryBlockNumber) {
this.emit('tx:block-update', txMeta, latestBlockNumber)
@ -123,7 +145,13 @@ module.exports = class PendingTransactionTracker extends EventEmitter {
this.emit('tx:retry', txMeta)
return txHash
}
/**
Ask the network for the transaction to see if it has been include in a block
@param txMeta {Object} - the txMeta object
@emits tx:failed
@emits tx:confirmed
@emits tx:warning
*/
async _checkPendingTx (txMeta) {
const txHash = txMeta.hash
const txId = txMeta.id
@ -162,8 +190,9 @@ module.exports = class PendingTransactionTracker extends EventEmitter {
}
}
// checks the network for signed txs and
// if confirmed sets the tx status as 'confirmed'
/**
checks the network for signed txs and releases the nonce global lock if it is
*/
async _checkPendingTxs () {
const signedTxList = this.getPendingTransactions()
// in order to keep the nonceTracker accurate we block it while updating pending transactions
@ -171,12 +200,17 @@ module.exports = class PendingTransactionTracker extends EventEmitter {
try {
await Promise.all(signedTxList.map((txMeta) => this._checkPendingTx(txMeta)))
} catch (err) {
console.error('PendingTransactionWatcher - Error updating pending transactions')
console.error(err)
log.error('PendingTransactionWatcher - Error updating pending transactions')
log.error(err)
}
nonceGlobalLock.releaseLock()
}
/**
checks to see if a confirmed txMeta has the same nonce
@param txMeta {Object} - txMeta object
@returns {boolean}
*/
async _checkIfNonceIsTaken (txMeta) {
const address = txMeta.txParams.from
const completed = this.getCompletedTransactions(address)
@ -185,5 +219,6 @@ module.exports = class PendingTransactionTracker extends EventEmitter {
})
return sameNonce.length > 0
}
}
module.exports = PendingTransactionTracker

View File

@ -3,22 +3,27 @@ const {
hexToBn,
BnMultiplyByFraction,
bnToHex,
} = require('./util')
} = require('../../lib/util')
const { addHexPrefix } = require('ethereumjs-util')
const SIMPLE_GAS_COST = '0x5208' // Hex for 21000, cost of a simple send.
/*
tx-utils are utility methods for Transaction manager
/**
tx-gas-utils are gas utility methods for Transaction manager
its passed ethquery
and used to do things like calculate gas of a tx.
@param {Object} provider - A network provider.
*/
module.exports = class TxGasUtil {
class TxGasUtil {
constructor (provider) {
this.query = new EthQuery(provider)
}
/**
@param txMeta {Object} - the txMeta object
@returns {object} the txMeta object with the gas written to the txParams
*/
async analyzeGasUsage (txMeta) {
const block = await this.query.getBlockByNumber('latest', true)
let estimatedGasHex
@ -38,6 +43,12 @@ module.exports = class TxGasUtil {
return txMeta
}
/**
Estimates the tx's gas usage
@param txMeta {Object} - the txMeta object
@param blockGasLimitHex {string} - hex string of the block's gas limit
@returns {string} the estimated gas limit as a hex string
*/
async estimateTxGas (txMeta, blockGasLimitHex) {
const txParams = txMeta.txParams
@ -70,6 +81,12 @@ module.exports = class TxGasUtil {
return await this.query.estimateGas(txParams)
}
/**
Writes the gas on the txParams in the txMeta
@param txMeta {Object} - the txMeta object to write to
@param blockGasLimitHex {string} - the block gas limit hex
@param estimatedGasHex {string} - the estimated gas hex
*/
setTxGas (txMeta, blockGasLimitHex, estimatedGasHex) {
txMeta.estimatedGas = addHexPrefix(estimatedGasHex)
const txParams = txMeta.txParams
@ -87,6 +104,13 @@ module.exports = class TxGasUtil {
return
}
/**
Adds a gas buffer with out exceeding the block gas limit
@param initialGasLimitHex {string} - the initial gas limit to add the buffer too
@param blockGasLimitHex {string} - the block gas limit
@returns {string} the buffered gas limit as a hex string
*/
addGasBuffer (initialGasLimitHex, blockGasLimitHex) {
const initialGasLimitBn = hexToBn(initialGasLimitHex)
const blockGasLimitBn = hexToBn(blockGasLimitHex)
@ -100,4 +124,6 @@ module.exports = class TxGasUtil {
// otherwise use blockGasLimit
return bnToHex(upperGasLimitBn)
}
}
}
module.exports = TxGasUtil

View File

@ -1,22 +1,33 @@
const extend = require('xtend')
const EventEmitter = require('events')
const ObservableStore = require('obs-store')
const createId = require('./random-id')
const ethUtil = require('ethereumjs-util')
const txStateHistoryHelper = require('./tx-state-history-helper')
// 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
module.exports = class TransactionStateManager extends EventEmitter {
const txStateHistoryHelper = require('./lib/tx-state-history-helper')
const createId = require('../../lib/random-id')
const { getFinalStates } = require('./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 opts {object}
@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()
@ -28,6 +39,10 @@ module.exports = class TransactionStateManager extends EventEmitter {
this.getNetwork = getNetwork
}
/**
@param opts {object} - the object to use when overwriting defaults
@returns {txMeta} the default txMeta object
*/
generateTxMeta (opts) {
return extend({
id: createId(),
@ -38,17 +53,25 @@ module.exports = class TransactionStateManager extends EventEmitter {
}, opts)
}
/**
@returns {array} of txMetas that have been filtered for only the current network
*/
getTxList () {
const network = this.getNetwork()
const fullTxList = this.getFullTxList()
return fullTxList.filter((txMeta) => txMeta.metamaskNetworkId === network)
}
/**
@returns {array} of all the txMetas in store
*/
getFullTxList () {
return this.store.getState().transactions
}
// Returns the tx list
/**
@returns {array} the tx list whos status is unapproved
*/
getUnapprovedTxList () {
const txList = this.getTxsByMetaData('status', 'unapproved')
return txList.reduce((result, tx) => {
@ -57,18 +80,37 @@ module.exports = 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
*/
getPendingTransactions (address) {
const opts = { status: 'submitted' }
if (address) opts.from = address
return this.getFilteredTxList(opts)
}
/**
@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
returns all txMetas who's status is confirmed for the current network
*/
getConfirmedTransactions (address) {
const opts = { status: 'confirmed' }
if (address) opts.from = address
return this.getFilteredTxList(opts)
}
/**
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 txMeta {Object}
@returns {object} the txMeta
*/
addTx (txMeta) {
this.once(`${txMeta.id}:signed`, function (txId) {
this.removeAllListeners(`${txMeta.id}:rejected`)
@ -92,7 +134,9 @@ module.exports = class TransactionStateManager extends EventEmitter {
// or rejected tx's.
// not tx's that are pending or unapproved
if (txCount > txHistoryLimit - 1) {
let index = transactions.findIndex((metaTx) => metaTx.status === 'confirmed' || metaTx.status === 'rejected')
const index = transactions.findIndex((metaTx) => {
return getFinalStates().includes(metaTx.status)
})
if (index !== -1) {
transactions.splice(index, 1)
}
@ -101,12 +145,21 @@ module.exports = class TransactionStateManager extends EventEmitter {
this._saveTxList(transactions)
return txMeta
}
// gets tx by Id and returns it
/**
@param txId {number}
@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 txMeta {Object} - the txMeta to update
@param [note] {string} - a not about the update for history
*/
updateTx (txMeta, note) {
// validate txParams
if (txMeta.txParams) {
@ -134,16 +187,23 @@ module.exports = class TransactionStateManager extends EventEmitter {
}
// merges txParams obj onto txData.txParams
// use extend to ensure that all fields are filled
/**
merges txParams obj onto txMeta.txParams
use extend to ensure that all fields are filled
@param txId {number} - the id of the txMeta
@param txParams {object} - the updated txParams
*/
updateTxParams (txId, txParams) {
const txMeta = this.getTx(txId)
txMeta.txParams = extend(txMeta.txParams, txParams)
this.updateTx(txMeta, `txStateManager#updateTxParams`)
}
// validates txParams members by type
validateTxParams(txParams) {
/**
validates txParams members by type
@param txParams {object} - txParams to validate
*/
validateTxParams (txParams) {
Object.keys(txParams).forEach((key) => {
const value = txParams[key]
// validate types
@ -159,17 +219,19 @@ module.exports = class TransactionStateManager extends EventEmitter {
})
}
/*
Takes an object of fields to search for eg:
let thingsToLookFor = {
to: '0x0..',
from: '0x0..',
status: 'signed',
err: undefined,
}
and returns a list of tx with all
/**
@param opts {object} - an object of fields to search for eg:<br>
let <code>thingsToLookFor = {<br>
to: '0x0..',<br>
from: '0x0..',<br>
status: 'signed',<br>
err: undefined,<br>
}<br></code>
@param [initialList=this.getTxList()]
@returns a {array} of txMeta with all
options matching
*/
/*
****************HINT****************
| `err: undefined` is like looking |
| for a tx with no err |
@ -190,10 +252,17 @@ module.exports = class TransactionStateManager extends EventEmitter {
})
return filteredTxList
}
/**
@param key {string} - the key to check
@param value - the value your looking for
@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
*/
getTxsByMetaData (key, value, txList = this.getTxList()) {
return txList.filter((txMeta) => {
if (txMeta.txParams[key]) {
if (key in txMeta.txParams) {
return txMeta.txParams[key] === value
} else {
return txMeta[key] === value
@ -203,33 +272,51 @@ module.exports = class TransactionStateManager extends EventEmitter {
// get::set status
// should return the status of the tx.
/**
@param txId {number} - the txMeta Id
@return {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'.
/**
should update the status of the tx to 'rejected'.
@param txId {number} - the txMeta Id
*/
setTxStatusRejected (txId) {
this._setTxStatus(txId, 'rejected')
}
// should update the status of the tx to 'unapproved'.
/**
should update the status of the tx to 'unapproved'.
@param txId {number} - the txMeta Id
*/
setTxStatusUnapproved (txId) {
this._setTxStatus(txId, 'unapproved')
}
// should update the status of the tx to 'approved'.
/**
should update the status of the tx to 'approved'.
@param txId {number} - the txMeta Id
*/
setTxStatusApproved (txId) {
this._setTxStatus(txId, 'approved')
}
// should update the status of the tx to 'signed'.
/**
should update the status of the tx to 'signed'.
@param txId {number} - the txMeta Id
*/
setTxStatusSigned (txId) {
this._setTxStatus(txId, 'signed')
}
// should update the status of the tx to 'submitted'.
// and add a time stamp for when it was called
/**
should update the status of the tx to 'submitted'.
and add a time stamp for when it was called
@param txId {number} - the txMeta Id
*/
setTxStatusSubmitted (txId) {
const txMeta = this.getTx(txId)
txMeta.submittedTime = (new Date()).getTime()
@ -237,17 +324,29 @@ module.exports = class TransactionStateManager extends EventEmitter {
this._setTxStatus(txId, 'submitted')
}
// should update the status of the tx to 'confirmed'.
/**
should update the status of the tx to 'confirmed'.
@param txId {number} - the txMeta Id
*/
setTxStatusConfirmed (txId) {
this._setTxStatus(txId, 'confirmed')
}
// should update the status dropped
/**
should update the status of the tx to 'dropped'.
@param txId {number} - the txMeta Id
*/
setTxStatusDropped (txId) {
this._setTxStatus(txId, 'dropped')
}
/**
should update the status of the tx to 'failed'.
and put the error on the txMeta
@param txId {number} - the txMeta Id
@param err {erroObject} - error object
*/
setTxStatusFailed (txId, err) {
const txMeta = this.getTx(txId)
txMeta.err = {
@ -258,6 +357,11 @@ module.exports = class TransactionStateManager extends EventEmitter {
this._setTxStatus(txId, 'failed')
}
/**
Removes transaction from the given address for the current network
from the txList
@param address {string} - hex string of the from address on the txParams to remove
*/
wipeTransactions (address) {
// network only tx
const txs = this.getFullTxList()
@ -273,9 +377,8 @@ module.exports = class TransactionStateManager extends EventEmitter {
// PRIVATE METHODS
//
// Should find the tx in the tx list and
// update it.
// should set the status in txData
// STATUS METHODS
// statuses:
// - `'unapproved'` the user has not responded
// - `'rejected'` the user has responded no!
// - `'approved'` the user has approved the tx
@ -283,6 +386,15 @@ module.exports = class TransactionStateManager extends EventEmitter {
// - `'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 txId {number} - the txMeta Id
@param status {string} - 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)
txMeta.status = status
@ -295,9 +407,14 @@ module.exports = class TransactionStateManager extends EventEmitter {
this.emit('update:badge')
}
// Saves the new/updated txList.
/**
Saves the new/updated txList.
@param transactions {array} - the list of transactions to save
*/
// Function is intended only for internal use
_saveTxList (transactions) {
this.store.updateState({ transactions })
}
}
module.exports = TransactionStateManager

View File

@ -1,6 +1,7 @@
// test and development environment variables
const env = process.env.METAMASK_ENV
const METAMASK_DEBUG = process.env.METAMASK_DEBUG
const { DEFAULT_NETWORK, MAINNET } = require('./controllers/network/enums')
/**
* @typedef {Object} FirstTimeState
@ -15,7 +16,7 @@ const initialState = {
config: {},
NetworkController: {
provider: {
type: (METAMASK_DEBUG || env === 'test') ? 'rinkeby' : 'mainnet',
type: (METAMASK_DEBUG || env === 'test') ? DEFAULT_NETWORK : MAINNET,
},
},
}

View File

@ -16,6 +16,24 @@ function noop () {}
class AccountTracker extends EventEmitter {
/**
* This module is responsible for tracking any number of accounts and caching their current balances & transaction
* counts.
*
* It also tracks transaction hashes, and checks their inclusion status on each new block.
*
* @typedef {Object} AccountTracker
* @param {Object} opts Initialize various properties of the class.
* @property {Object} store The stored object containing all accounts to track, as well as the current block's gas limit.
* @property {Object} store.accounts The accounts currently stored in this AccountTracker
* @property {string} store.currentBlockGasLimit A hex string indicating the gas limit of the current block
* @property {Object} _provider A provider needed to create the EthQuery instance used within this AccountTracker.
* @property {EthQuery} _query An EthQuery instance used to access account information from the blockchain
* @property {BlockTracker} _blockTracker A BlockTracker instance. Needed to ensure that accounts and their info updates
* when a new block is created.
* @property {Object} _currentBlockNumber Reference to a property on the _blockTracker: the number (i.e. an id) of the the current block
*
*/
constructor (opts = {}) {
super()
@ -34,10 +52,17 @@ class AccountTracker extends EventEmitter {
this._currentBlockNumber = this._blockTracker.currentBlock
}
//
// public
//
/**
* Ensures that the locally stored accounts are in sync with a set of accounts stored externally to this
* AccountTracker.
*
* Once this AccountTracker's accounts are up to date with those referenced by the passed addresses, each
* of these accounts are given an updated balance via EthQuery.
*
* @param {array} address The array of hex addresses for accounts with which this AccountTracker's accounts should be
* in sync
*
*/
syncWithAddresses (addresses) {
const accounts = this.store.getState().accounts
const locals = Object.keys(accounts)
@ -61,6 +86,13 @@ class AccountTracker extends EventEmitter {
this._updateAccounts()
}
/**
* Adds a new address to this AccountTracker's accounts object, which points to an empty object. This object will be
* given a balance as long this._currentBlockNumber is defined.
*
* @param {string} address A hex address of a new account to store in this AccountTracker's accounts object
*
*/
addAccount (address) {
const accounts = this.store.getState().accounts
accounts[address] = {}
@ -69,16 +101,27 @@ class AccountTracker extends EventEmitter {
this._updateAccount(address)
}
/**
* Removes an account from this AccountTracker's accounts object
*
* @param {string} address A hex address of a the account to remove
*
*/
removeAccount (address) {
const accounts = this.store.getState().accounts
delete accounts[address]
this.store.updateState({ accounts })
}
//
// private
//
/**
* Given a block, updates this AccountTracker's currentBlockGasLimit, and then updates each local account's balance
* via EthQuery
*
* @private
* @param {object} block Data about the block that contains the data to update to.
* @fires 'block' The updated state, if all account updates are successful
*
*/
_updateForBlock (block) {
this._currentBlockNumber = block.number
const currentBlockGasLimit = block.gasLimit
@ -93,12 +136,26 @@ class AccountTracker extends EventEmitter {
})
}
/**
* Calls this._updateAccount for each account in this.store
*
* @param {Function} cb A callback to pass to this._updateAccount, called after each account is successfully updated
*
*/
_updateAccounts (cb = noop) {
const accounts = this.store.getState().accounts
const addresses = Object.keys(accounts)
async.each(addresses, this._updateAccount.bind(this), cb)
}
/**
* Updates the current balance of an account. Gets an updated balance via this._getAccount.
*
* @private
* @param {string} address A hex address of a the account to be updated
* @param {Function} cb A callback to call once the account at address is successfully update
*
*/
_updateAccount (address, cb = noop) {
this._getAccount(address, (err, result) => {
if (err) return cb(err)
@ -113,6 +170,14 @@ class AccountTracker extends EventEmitter {
})
}
/**
* Gets the current balance of an account via EthQuery.
*
* @private
* @param {string} address A hex address of a the account to query
* @param {Function} cb A callback to call once the account at address is successfully update
*
*/
_getAccount (address, cb = noop) {
const query = this._query
async.parallel({

View File

@ -1,12 +1,11 @@
const ethUtil = require('ethereumjs-util')
const normalize = require('eth-sig-util').normalize
const MetamaskConfig = require('../config.js')
const MAINNET_RPC = MetamaskConfig.network.mainnet
const ROPSTEN_RPC = MetamaskConfig.network.ropsten
const KOVAN_RPC = MetamaskConfig.network.kovan
const RINKEBY_RPC = MetamaskConfig.network.rinkeby
const {
MAINNET_RPC_URL,
ROPSTEN_RPC_URL,
KOVAN_RPC_URL,
RINKEBY_RPC_URL,
} = require('../controllers/network/enums')
/* The config-manager is a convenience object
* wrapping a pojo-migrator.
@ -102,6 +101,7 @@ ConfigManager.prototype.setShowSeedWords = function (should) {
this.setData(data)
}
ConfigManager.prototype.getShouldShowSeedWords = function () {
var data = this.getData()
return data.showSeedWords
@ -117,27 +117,6 @@ ConfigManager.prototype.getSeedWords = function () {
var data = this.getData()
return data.seedWords
}
/**
* Called to set the isRevealingSeedWords flag. This happens only when the user chooses to reveal
* the seed words and not during the first time flow.
* @param {boolean} reveal - Value to set the isRevealingSeedWords flag.
*/
ConfigManager.prototype.setIsRevealingSeedWords = function (reveal = false) {
const data = this.getData()
data.isRevealingSeedWords = reveal
this.setData(data)
}
/**
* Returns the isRevealingSeedWords flag.
* @returns {boolean|undefined}
*/
ConfigManager.prototype.getIsRevealingSeedWords = function () {
const data = this.getData()
return data.isRevealingSeedWords
}
ConfigManager.prototype.setRpcTarget = function (rpcUrl) {
var config = this.getConfig()
config.provider = {
@ -174,19 +153,19 @@ ConfigManager.prototype.getCurrentRpcAddress = function () {
switch (provider.type) {
case 'mainnet':
return MAINNET_RPC
return MAINNET_RPC_URL
case 'ropsten':
return ROPSTEN_RPC
return ROPSTEN_RPC_URL
case 'kovan':
return KOVAN_RPC
return KOVAN_RPC_URL
case 'rinkeby':
return RINKEBY_RPC
return RINKEBY_RPC_URL
default:
return provider && provider.rpcTarget ? provider.rpcTarget : RINKEBY_RPC
return provider && provider.rpcTarget ? provider.rpcTarget : RINKEBY_RPC_URL
}
}

View File

@ -4,17 +4,18 @@ const errorLabelPrefix = 'Error: '
module.exports = extractEthjsErrorMessage
//
// ethjs-rpc provides overly verbose error messages
// if we detect this type of message, we extract the important part
// Below is an example input and output
//
// Error: [ethjs-rpc] rpc error with payload {"id":3947817945380,"jsonrpc":"2.0","params":["0xf8eb8208708477359400830398539406012c8cf97bead5deae237070f9587f8e7a266d80b8843d7d3f5a0000000000000000000000000000000000000000000000000000000000081d1a000000000000000000000000000000000000000000000000001ff973cafa800000000000000000000000000000000000000000000000000000038d7ea4c68000000000000000000000000000000000000000000000000000000000000003f48025a04c32a9b630e0d9e7ff361562d850c86b7a884908135956a7e4a336fa0300d19ca06830776423f25218e8d19b267161db526e66895567147015b1f3fc47aef9a3c7"],"method":"eth_sendRawTransaction"} Error: replacement transaction underpriced
//
// Transaction Failed: replacement transaction underpriced
//
/**
* 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
*
* @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) {
const isEthjsRpcError = errorMessage.includes(ethJsRpcSlug)
if (isEthjsRpcError) {

View File

@ -14,6 +14,15 @@ 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
* replaced with the javascript type of that value.
*
*/
function getObjStructure(obj) {
const structure = clone(obj)
return deepMap(structure, (value) => {
@ -21,6 +30,14 @@ 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
*/
function deepMap(target = {}, visit) {
Object.entries(target).forEach(([key, value]) => {
if (typeof value === 'object' && value !== null) {

View File

@ -3,8 +3,37 @@ const ObservableStore = require('obs-store')
const ethUtil = require('ethereumjs-util')
const createId = require('./random-id')
/**
* Represents, and contains data about, an 'eth_sign' type signature request. These are created when a signature for
* an eth_sign call is requested.
*
* @see {@link https://github.com/ethereum/wiki/wiki/JSON-RPC#eth_sign}
*
* @typedef {Object} Message
* @property {number} id An id to track and identify the message object
* @property {Object} msgParams The parameters to pass to the eth_sign method once the signature request is approved.
* @property {Object} msgParams.metamaskId Added to msgParams for tracking and identification within MetaMask.
* @property {string} msgParams.data A hex string conversion of the raw buffer data of the signature request
* @property {number} time The epoch time at which the this message was created
* @property {string} status Indicates whether the signature request is 'unapproved', 'approved', 'signed' or 'rejected'
* @property {string} type The json-prc signing method for which a signature request has been made. A 'Message' with
* always have a 'eth_sign' type.
*
*/
module.exports = class MessageManager extends EventEmitter {
/**
* Controller in charge of managing - storing, adding, removing, updating - Messages.
*
* @typedef {Object} MessageManager
* @param {Object} opts @deprecated
* @property {Object} memStore The observable store where Messages are saved.
* @property {Object} memStore.unapprovedMsgs A collection of all Messages in the 'unapproved' state
* @property {number} memStore.unapprovedMsgCount The count of all Messages in this.memStore.unapprobedMsgs
* @property {array} messages Holds all messages that have been created by this MessageManager
*
*/
constructor (opts) {
super()
this.memStore = new ObservableStore({
@ -14,15 +43,35 @@ module.exports = class MessageManager extends EventEmitter {
this.messages = []
}
/**
* A getter for the number of 'unapproved' Messages in this.messages
*
* @returns {number} The number of 'unapproved' Messages in this.messages
*
*/
get unapprovedMsgCount () {
return Object.keys(this.getUnapprovedMsgs()).length
}
/**
* A getter for the 'unapproved' Messages in this.messages
*
* @returns {Object} An index of Message ids to Messages, for all 'unapproved' Messages in this.messages
*
*/
getUnapprovedMsgs () {
return this.messages.filter(msg => msg.status === 'unapproved')
.reduce((result, msg) => { result[msg.id] = msg; return result }, {})
}
/**
* Creates a new Message with an 'unapproved' status using the passed msgParams. this.addMsg is called to add the
* new Message to this.messages, and to save the unapproved Messages from that list to this.memStore.
*
* @param {Object} msgParams The params for the eth_sign call to be made after the message is approved.
* @returns {number} The id of the newly created message.
*
*/
addUnapprovedMessage (msgParams) {
msgParams.data = normalizeMsgData(msgParams.data)
// create txData obj with parameters and meta data
@ -42,24 +91,61 @@ module.exports = class MessageManager extends EventEmitter {
return msgId
}
/**
* Adds a passed Message to this.messages, and calls this._saveMsgList() to save the unapproved Messages from that
* list to this.memStore.
*
* @param {Message} msg The Message to add to this.messages
*
*/
addMsg (msg) {
this.messages.push(msg)
this._saveMsgList()
}
/**
* Returns a specified Message.
*
* @param {number} msgId The id of the Message to get
* @returns {Message|undefined} The Message with the id that matches the passed msgId, or undefined if no Message has that id.
*
*/
getMsg (msgId) {
return this.messages.find(msg => msg.id === msgId)
}
/**
* Approves a Message. Sets the message status via a call to this.setMsgStatusApproved, and returns a promise with
* any the message params modified for proper signing.
*
* @param {Object} msgParams The msgParams to be used when eth_sign is called, plus data added by MetaMask.
* @param {Object} msgParams.metamaskId Added to msgParams for tracking and identification within MetaMask.
* @returns {Promise<object>} Promises the msgParams object with metamaskId removed.
*
*/
approveMessage (msgParams) {
this.setMsgStatusApproved(msgParams.metamaskId)
return this.prepMsgForSigning(msgParams)
}
/**
* Sets a Message status to 'approved' via a call to this._setMsgStatus.
*
* @param {number} msgId The id of the Message to approve.
*
*/
setMsgStatusApproved (msgId) {
this._setMsgStatus(msgId, 'approved')
}
/**
* Sets a Message status to 'signed' via a call to this._setMsgStatus and updates that Message in this.messages by
* adding the raw signature data of the signature request to the Message
*
* @param {number} msgId The id of the Message to sign.
* @param {buffer} rawSig The raw data of the signature request
*
*/
setMsgStatusSigned (msgId, rawSig) {
const msg = this.getMsg(msgId)
msg.rawSig = rawSig
@ -67,19 +153,40 @@ module.exports = class MessageManager extends EventEmitter {
this._setMsgStatus(msgId, 'signed')
}
/**
* Removes the metamaskId property from passed msgParams and returns a promise which resolves the updated msgParams
*
* @param {Object} msgParams The msgParams to modify
* @returns {Promise<object>} Promises the msgParams with the metamaskId property removed
*
*/
prepMsgForSigning (msgParams) {
delete msgParams.metamaskId
return Promise.resolve(msgParams)
}
/**
* Sets a Message status to 'rejected' via a call to this._setMsgStatus.
*
* @param {number} msgId The id of the Message to reject.
*
*/
rejectMsg (msgId) {
this._setMsgStatus(msgId, 'rejected')
}
//
// PRIVATE METHODS
//
/**
* Updates the status of a Message in this.messages via a call to this._updateMsg
*
* @private
* @param {number} msgId The id of the Message to update.
* @param {string} status The new status of the Message.
* @throws A 'MessageManager - Message not found for id: "${msgId}".' if there is no Message in this.messages with an
* id equal to the passed msgId
* @fires An event with a name equal to `${msgId}:${status}`. The Message is also fired.
* @fires If status is 'rejected' or 'signed', an event with a name equal to `${msgId}:finished` is fired along with the message
*
*/
_setMsgStatus (msgId, status) {
const msg = this.getMsg(msgId)
if (!msg) throw new Error('MessageManager - Message not found for id: "${msgId}".')
@ -91,6 +198,14 @@ module.exports = class MessageManager extends EventEmitter {
}
}
/**
* Sets a Message in this.messages to the passed Message if the ids are equal. Then saves the unapprovedMsg list to
* storage via this._saveMsgList
*
* @private
* @param {msg} Message A Message that will replace an existing Message (with the same id) in this.messages
*
*/
_updateMsg (msg) {
const index = this.messages.findIndex((message) => message.id === msg.id)
if (index !== -1) {
@ -99,6 +214,13 @@ module.exports = class MessageManager extends EventEmitter {
this._saveMsgList()
}
/**
* Saves the unapproved messages, and their count, to this.memStore
*
* @private
* @fires 'updateBadge'
*
*/
_saveMsgList () {
const unapprovedMsgs = this.getUnapprovedMsgs()
const unapprovedMsgCount = Object.keys(unapprovedMsgs).length
@ -108,6 +230,13 @@ module.exports = class MessageManager extends EventEmitter {
}
/**
* A helper function that converts raw buffer data to a hex, or just returns the data if it is already formatted as a hex.
*
* @param {any} data The buffer data to convert to a hex
* @returns {string} A hex string conversion of the buffer data
*
*/
function normalizeMsgData (data) {
if (data.slice(0, 2) === '0x') {
// data is already hex

View File

@ -5,10 +5,18 @@ const width = 360
class NotificationManager {
//
// Public
//
/**
* A collection of methods for controlling the showing and hiding of the notification popup.
*
* @typedef {Object} NotificationManager
*
*/
/**
* Either brings an existing MetaMask notification window into focus, or creates a new notification window. New
* notification windows are given a 'popup' type.
*
*/
showPopup () {
this._getPopup((err, popup) => {
if (err) throw err
@ -29,6 +37,10 @@ class NotificationManager {
})
}
/**
* Closes a MetaMask notification if it window exists.
*
*/
closePopup () {
// closes notification popup
this._getPopup((err, popup) => {
@ -38,10 +50,14 @@ class NotificationManager {
})
}
//
// Private
//
/**
* Checks all open MetaMask windows, and returns the first one it finds that is a notification window (i.e. has the
* type 'popup')
*
* @private
* @param {Function} cb A node style callback that to whcih the found notification window will be passed.
*
*/
_getPopup (cb) {
this._getWindows((err, windows) => {
if (err) throw err
@ -49,6 +65,13 @@ class NotificationManager {
})
}
/**
* Returns all open MetaMask windows.
*
* @private
* @param {Function} cb A node style callback that to which the windows will be passed.
*
*/
_getWindows (cb) {
// Ignore in test environment
if (!extension.windows) {
@ -60,6 +83,13 @@ class NotificationManager {
})
}
/**
* Given an array of windows, returns the first that has a 'popup' type, or null if no such window exists.
*
* @private
* @param {array} windows An array of objects containing data about the open MetaMask extension windows.
*
*/
_getPopupIn (windows) {
return windows ? windows.find((win) => {
// Returns notification popup

View File

@ -3,16 +3,28 @@ const normalize = require('eth-sig-util').normalize
class PendingBalanceCalculator {
// Must be initialized with two functions:
// getBalance => Returns a promise of a BN of the current balance in Wei
// getPendingTransactions => Returns an array of TxMeta Objects,
// which have txParams properties, which include value, gasPrice, and gas,
// all in a base=16 hex format.
/**
* Used for calculating a users "pending balance": their current balance minus the total possible cost of all their
* pending transactions.
*
* @typedef {Object} PendingBalanceCalculator
* @param {Function} getBalance Returns a promise of a BN of the current balance in Wei
* @param {Function} getPendingTransactions Returns an array of TxMeta Objects, which have txParams properties,
* which include value, gasPrice, and gas, all in a base=16 hex format.
*
*/
constructor ({ getBalance, getPendingTransactions }) {
this.getPendingTransactions = getPendingTransactions
this.getNetworkBalance = getBalance
}
/**
* Returns the users "pending balance": their current balance minus the total possible cost of all their
* pending transactions.
*
* @returns {Promise<string>} Promises a base 16 hex string that contains the user's "pending balance"
*
*/
async getBalance () {
const results = await Promise.all([
this.getNetworkBalance(),
@ -29,6 +41,15 @@ class PendingBalanceCalculator {
return `0x${balance.sub(pendingValue).toString(16)}`
}
/**
* Calculates the maximum possible cost of a single transaction, based on the value, gas price and gas limit.
*
* @param {object} tx Contains all that data about a transaction.
* @property {object} tx.txParams Contains data needed to calculate the maximum cost of the transaction: gas,
* gasLimit and value.
*
* @returns {string} Returns a base 16 hex string that contains the maximum possible cost of the transaction.
*/
calculateMaxCost (tx) {
const txValue = tx.txParams.value
const value = this.hexToBn(txValue)
@ -42,6 +63,13 @@ class PendingBalanceCalculator {
return value.add(gasCost)
}
/**
* Converts a hex string to a BN object
*
* @param {string} hex A number represented as a hex string
* @returns {Object} A BN object
*
*/
hexToBn (hex) {
return new BN(normalize(hex).substring(2), 16)
}

View File

@ -5,8 +5,37 @@ const createId = require('./random-id')
const hexRe = /^[0-9A-Fa-f]+$/g
const log = require('loglevel')
/**
* Represents, and contains data about, an 'personal_sign' type signature request. These are created when a
* signature for an personal_sign call is requested.
*
* @see {@link https://web3js.readthedocs.io/en/1.0/web3-eth-personal.html#sign}
*
* @typedef {Object} PersonalMessage
* @property {number} id An id to track and identify the message object
* @property {Object} msgParams The parameters to pass to the personal_sign method once the signature request is
* approved.
* @property {Object} msgParams.metamaskId Added to msgParams for tracking and identification within MetaMask.
* @property {string} msgParams.data A hex string conversion of the raw buffer data of the signature request
* @property {number} time The epoch time at which the this message was created
* @property {string} status Indicates whether the signature request is 'unapproved', 'approved', 'signed' or 'rejected'
* @property {string} type The json-prc signing method for which a signature request has been made. A 'Message' will
* always have a 'personal_sign' type.
*
*/
module.exports = class PersonalMessageManager extends EventEmitter {
/**
* Controller in charge of managing - storing, adding, removing, updating - PersonalMessage.
*
* @typedef {Object} PersonalMessageManager
* @param {Object} opts @deprecated
* @property {Object} memStore The observable store where PersonalMessage are saved with persistance.
* @property {Object} memStore.unapprovedPersonalMsgs A collection of all PersonalMessages in the 'unapproved' state
* @property {number} memStore.unapprovedPersonalMsgCount The count of all PersonalMessages in this.memStore.unapprobedMsgs
* @property {array} messages Holds all messages that have been created by this PersonalMessageManager
*
*/
constructor (opts) {
super()
this.memStore = new ObservableStore({
@ -16,15 +45,37 @@ module.exports = class PersonalMessageManager extends EventEmitter {
this.messages = []
}
/**
* A getter for the number of 'unapproved' PersonalMessages in this.messages
*
* @returns {number} The number of 'unapproved' PersonalMessages in this.messages
*
*/
get unapprovedPersonalMsgCount () {
return Object.keys(this.getUnapprovedMsgs()).length
}
/**
* A getter for the 'unapproved' PersonalMessages in this.messages
*
* @returns {Object} An index of PersonalMessage ids to PersonalMessages, for all 'unapproved' PersonalMessages in
* this.messages
*
*/
getUnapprovedMsgs () {
return this.messages.filter(msg => msg.status === 'unapproved')
.reduce((result, msg) => { result[msg.id] = msg; return result }, {})
}
/**
* Creates a new PersonalMessage with an 'unapproved' status using the passed msgParams. this.addMsg is called to add
* the new PersonalMessage to this.messages, and to save the unapproved PersonalMessages from that list to
* this.memStore.
*
* @param {Object} msgParams The params for the eth_sign call to be made after the message is approved.
* @returns {number} The id of the newly created PersonalMessage.
*
*/
addUnapprovedMessage (msgParams) {
log.debug(`PersonalMessageManager addUnapprovedMessage: ${JSON.stringify(msgParams)}`)
msgParams.data = this.normalizeMsgData(msgParams.data)
@ -45,24 +96,62 @@ module.exports = class PersonalMessageManager extends EventEmitter {
return msgId
}
/**
* Adds a passed PersonalMessage to this.messages, and calls this._saveMsgList() to save the unapproved PersonalMessages from that
* list to this.memStore.
*
* @param {Message} msg The PersonalMessage to add to this.messages
*
*/
addMsg (msg) {
this.messages.push(msg)
this._saveMsgList()
}
/**
* Returns a specified PersonalMessage.
*
* @param {number} msgId The id of the PersonalMessage to get
* @returns {PersonalMessage|undefined} The PersonalMessage with the id that matches the passed msgId, or undefined
* if no PersonalMessage has that id.
*
*/
getMsg (msgId) {
return this.messages.find(msg => msg.id === msgId)
}
/**
* Approves a PersonalMessage. Sets the message status via a call to this.setMsgStatusApproved, and returns a promise
* with any the message params modified for proper signing.
*
* @param {Object} msgParams The msgParams to be used when eth_sign is called, plus data added by MetaMask.
* @param {Object} msgParams.metamaskId Added to msgParams for tracking and identification within MetaMask.
* @returns {Promise<object>} Promises the msgParams object with metamaskId removed.
*
*/
approveMessage (msgParams) {
this.setMsgStatusApproved(msgParams.metamaskId)
return this.prepMsgForSigning(msgParams)
}
/**
* Sets a PersonalMessage status to 'approved' via a call to this._setMsgStatus.
*
* @param {number} msgId The id of the PersonalMessage to approve.
*
*/
setMsgStatusApproved (msgId) {
this._setMsgStatus(msgId, 'approved')
}
/**
* Sets a PersonalMessage status to 'signed' via a call to this._setMsgStatus and updates that PersonalMessage in
* this.messages by adding the raw signature data of the signature request to the PersonalMessage
*
* @param {number} msgId The id of the PersonalMessage to sign.
* @param {buffer} rawSig The raw data of the signature request
*
*/
setMsgStatusSigned (msgId, rawSig) {
const msg = this.getMsg(msgId)
msg.rawSig = rawSig
@ -70,19 +159,41 @@ module.exports = class PersonalMessageManager extends EventEmitter {
this._setMsgStatus(msgId, 'signed')
}
/**
* Removes the metamaskId property from passed msgParams and returns a promise which resolves the updated msgParams
*
* @param {Object} msgParams The msgParams to modify
* @returns {Promise<object>} Promises the msgParams with the metamaskId property removed
*
*/
prepMsgForSigning (msgParams) {
delete msgParams.metamaskId
return Promise.resolve(msgParams)
}
/**
* Sets a PersonalMessage status to 'rejected' via a call to this._setMsgStatus.
*
* @param {number} msgId The id of the PersonalMessage to reject.
*
*/
rejectMsg (msgId) {
this._setMsgStatus(msgId, 'rejected')
}
//
// PRIVATE METHODS
//
/**
* Updates the status of a PersonalMessage in this.messages via a call to this._updateMsg
*
* @private
* @param {number} msgId The id of the PersonalMessage to update.
* @param {string} status The new status of the PersonalMessage.
* @throws A 'PersonalMessageManager - PersonalMessage not found for id: "${msgId}".' if there is no PersonalMessage
* in this.messages with an id equal to the passed msgId
* @fires An event with a name equal to `${msgId}:${status}`. The PersonalMessage is also fired.
* @fires If status is 'rejected' or 'signed', an event with a name equal to `${msgId}:finished` is fired along
* with the PersonalMessage
*
*/
_setMsgStatus (msgId, status) {
const msg = this.getMsg(msgId)
if (!msg) throw new Error('PersonalMessageManager - Message not found for id: "${msgId}".')
@ -94,6 +205,15 @@ module.exports = class PersonalMessageManager extends EventEmitter {
}
}
/**
* Sets a PersonalMessage in this.messages to the passed PersonalMessage if the ids are equal. Then saves the
* unapprovedPersonalMsgs index to storage via this._saveMsgList
*
* @private
* @param {msg} PersonalMessage A PersonalMessage that will replace an existing PersonalMessage (with the same
* id) in this.messages
*
*/
_updateMsg (msg) {
const index = this.messages.findIndex((message) => message.id === msg.id)
if (index !== -1) {
@ -102,6 +222,13 @@ module.exports = class PersonalMessageManager extends EventEmitter {
this._saveMsgList()
}
/**
* Saves the unapproved PersonalMessages, and their count, to this.memStore
*
* @private
* @fires 'updateBadge'
*
*/
_saveMsgList () {
const unapprovedPersonalMsgs = this.getUnapprovedMsgs()
const unapprovedPersonalMsgCount = Object.keys(unapprovedPersonalMsgs).length
@ -109,6 +236,13 @@ module.exports = class PersonalMessageManager extends EventEmitter {
this.emit('updateBadge')
}
/**
* A helper function that converts raw buffer data to a hex, or just returns the data if it is already formatted as a hex.
*
* @param {any} data The buffer data to convert to a hex
* @returns {string} A hex string conversion of the buffer data
*
*/
normalizeMsgData (data) {
try {
const stripped = ethUtil.stripHexPrefix(data)

View File

@ -3,11 +3,19 @@ const log = require('loglevel')
const seedPhraseVerifier = {
// Verifies if the seed words can restore the accounts.
//
// The seed words can recreate the primary keyring and the accounts belonging to it.
// The created accounts in the primary keyring are always the same.
// The keyring always creates the accounts in the same sequence.
/**
* Verifies if the seed words can restore the accounts.
*
* Key notes:
* - The seed words can recreate the primary keyring and the accounts belonging to it.
* - The created accounts in the primary keyring are always the same.
* - The keyring always creates the accounts in the same sequence.
*
* @param {array} createdAccounts The accounts to restore
* @param {string} seedWords The seed words to verify
* @returns {Promise<void>} Promises undefined
*
*/
verifyAccounts (createdAccounts, seedWords) {
return new Promise((resolve, reject) => {

View File

@ -23,22 +23,16 @@ function setupRaven(opts) {
release,
transport: function(opts) {
const report = opts.data
// simplify certain complex error messages
report.exception.values.forEach(item => {
let errorMessage = item.value
// simplify ethjs error messages
errorMessage = extractEthjsErrorMessage(errorMessage)
// simplify 'Transaction Failed: known transaction'
if (errorMessage.indexOf('Transaction Failed: known transaction') === 0) {
// cut the hash from the error message
errorMessage = 'Transaction Failed: known transaction'
}
// finalize
item.value = errorMessage
})
// modify report urls
rewriteReportUrls(report)
try {
// handle error-like non-error exceptions
nonErrorException(report)
// simplify certain complex error messages (e.g. Ethjs)
simplifyErrorMessages(report)
// modify report urls
rewriteReportUrls(report)
} catch (err) {
console.warn(err)
}
// make request normally
client._makeRequest(opts)
},
@ -48,15 +42,42 @@ function setupRaven(opts) {
return Raven
}
function nonErrorException(report) {
// handle errors that lost their error-ness in serialization
if (report.message.includes('Non-Error exception captured with keys: message')) {
if (!(report.extra && report.extra.__serialized__)) return
report.message = `Non-Error Exception: ${report.extra.__serialized__.message}`
}
}
function simplifyErrorMessages(report) {
if (report.exception && report.exception.values) {
report.exception.values.forEach(item => {
let errorMessage = item.value
// simplify ethjs error messages
errorMessage = extractEthjsErrorMessage(errorMessage)
// simplify 'Transaction Failed: known transaction'
if (errorMessage.indexOf('Transaction Failed: known transaction') === 0) {
// cut the hash from the error message
errorMessage = 'Transaction Failed: known transaction'
}
// finalize
item.value = errorMessage
})
}
}
function rewriteReportUrls(report) {
// update request url
report.request.url = toMetamaskUrl(report.request.url)
// update exception stack trace
report.exception.values.forEach(item => {
item.stacktrace.frames.forEach(frame => {
frame.filename = toMetamaskUrl(frame.filename)
if (report.exception && report.exception.values) {
report.exception.values.forEach(item => {
item.stacktrace.frames.forEach(frame => {
frame.filename = toMetamaskUrl(frame.filename)
})
})
})
}
}
function toMetamaskUrl(origUrl) {

View File

@ -5,7 +5,36 @@ const assert = require('assert')
const sigUtil = require('eth-sig-util')
const log = require('loglevel')
/**
* Represents, and contains data about, an 'eth_signTypedData' type signature request. These are created when a
* signature for an eth_signTypedData call is requested.
*
* @typedef {Object} TypedMessage
* @property {number} id An id to track and identify the message object
* @property {Object} msgParams The parameters to pass to the eth_signTypedData method once the signature request is
* approved.
* @property {Object} msgParams.metamaskId Added to msgParams for tracking and identification within MetaMask.
* @property {Object} msgParams.from The address that is making the signature request.
* @property {string} msgParams.data A hex string conversion of the raw buffer data of the signature request
* @property {number} time The epoch time at which the this message was created
* @property {string} status Indicates whether the signature request is 'unapproved', 'approved', 'signed' or 'rejected'
* @property {string} type The json-prc signing method for which a signature request has been made. A 'Message' will
* always have a 'eth_signTypedData' type.
*
*/
module.exports = class TypedMessageManager extends EventEmitter {
/**
* Controller in charge of managing - storing, adding, removing, updating - TypedMessage.
*
* @typedef {Object} TypedMessage
* @param {Object} opts @deprecated
* @property {Object} memStore The observable store where TypedMessage are saved.
* @property {Object} memStore.unapprovedTypedMessages A collection of all TypedMessages in the 'unapproved' state
* @property {number} memStore.unapprovedTypedMessagesCount The count of all TypedMessages in this.memStore.unapprobedMsgs
* @property {array} messages Holds all messages that have been created by this TypedMessage
*
*/
constructor (opts) {
super()
this.memStore = new ObservableStore({
@ -15,15 +44,37 @@ module.exports = class TypedMessageManager extends EventEmitter {
this.messages = []
}
/**
* A getter for the number of 'unapproved' TypedMessages in this.messages
*
* @returns {number} The number of 'unapproved' TypedMessages in this.messages
*
*/
get unapprovedTypedMessagesCount () {
return Object.keys(this.getUnapprovedMsgs()).length
}
/**
* A getter for the 'unapproved' TypedMessages in this.messages
*
* @returns {Object} An index of TypedMessage ids to TypedMessages, for all 'unapproved' TypedMessages in
* this.messages
*
*/
getUnapprovedMsgs () {
return this.messages.filter(msg => msg.status === 'unapproved')
.reduce((result, msg) => { result[msg.id] = msg; return result }, {})
}
/**
* Creates a new TypedMessage with an 'unapproved' status using the passed msgParams. this.addMsg is called to add
* the new TypedMessage to this.messages, and to save the unapproved TypedMessages from that list to
* this.memStore. Before any of this is done, msgParams are validated
*
* @param {Object} msgParams The params for the eth_sign call to be made after the message is approved.
* @returns {number} The id of the newly created TypedMessage.
*
*/
addUnapprovedMessage (msgParams) {
this.validateParams(msgParams)
@ -45,6 +96,12 @@ module.exports = class TypedMessageManager extends EventEmitter {
return msgId
}
/**
* Helper method for this.addUnapprovedMessage. Validates that the passed params have the required properties.
*
* @param {Object} params The params to validate
*
*/
validateParams (params) {
assert.equal(typeof params, 'object', 'Params should ben an object.')
assert.ok('data' in params, 'Params must include a data field.')
@ -56,24 +113,62 @@ module.exports = class TypedMessageManager extends EventEmitter {
}, 'Expected EIP712 typed data')
}
/**
* Adds a passed TypedMessage to this.messages, and calls this._saveMsgList() to save the unapproved TypedMessages from that
* list to this.memStore.
*
* @param {Message} msg The TypedMessage to add to this.messages
*
*/
addMsg (msg) {
this.messages.push(msg)
this._saveMsgList()
}
/**
* Returns a specified TypedMessage.
*
* @param {number} msgId The id of the TypedMessage to get
* @returns {TypedMessage|undefined} The TypedMessage with the id that matches the passed msgId, or undefined
* if no TypedMessage has that id.
*
*/
getMsg (msgId) {
return this.messages.find(msg => msg.id === msgId)
}
/**
* Approves a TypedMessage. Sets the message status via a call to this.setMsgStatusApproved, and returns a promise
* with any the message params modified for proper signing.
*
* @param {Object} msgParams The msgParams to be used when eth_sign is called, plus data added by MetaMask.
* @param {Object} msgParams.metamaskId Added to msgParams for tracking and identification within MetaMask.
* @returns {Promise<object>} Promises the msgParams object with metamaskId removed.
*
*/
approveMessage (msgParams) {
this.setMsgStatusApproved(msgParams.metamaskId)
return this.prepMsgForSigning(msgParams)
}
/**
* Sets a TypedMessage status to 'approved' via a call to this._setMsgStatus.
*
* @param {number} msgId The id of the TypedMessage to approve.
*
*/
setMsgStatusApproved (msgId) {
this._setMsgStatus(msgId, 'approved')
}
/**
* Sets a TypedMessage status to 'signed' via a call to this._setMsgStatus and updates that TypedMessage in
* this.messages by adding the raw signature data of the signature request to the TypedMessage
*
* @param {number} msgId The id of the TypedMessage to sign.
* @param {buffer} rawSig The raw data of the signature request
*
*/
setMsgStatusSigned (msgId, rawSig) {
const msg = this.getMsg(msgId)
msg.rawSig = rawSig
@ -81,11 +176,24 @@ module.exports = class TypedMessageManager extends EventEmitter {
this._setMsgStatus(msgId, 'signed')
}
/**
* Removes the metamaskId property from passed msgParams and returns a promise which resolves the updated msgParams
*
* @param {Object} msgParams The msgParams to modify
* @returns {Promise<object>} Promises the msgParams with the metamaskId property removed
*
*/
prepMsgForSigning (msgParams) {
delete msgParams.metamaskId
return Promise.resolve(msgParams)
}
/**
* Sets a TypedMessage status to 'rejected' via a call to this._setMsgStatus.
*
* @param {number} msgId The id of the TypedMessage to reject.
*
*/
rejectMsg (msgId) {
this._setMsgStatus(msgId, 'rejected')
}
@ -94,6 +202,19 @@ module.exports = class TypedMessageManager extends EventEmitter {
// PRIVATE METHODS
//
/**
* Updates the status of a TypedMessage in this.messages via a call to this._updateMsg
*
* @private
* @param {number} msgId The id of the TypedMessage to update.
* @param {string} status The new status of the TypedMessage.
* @throws A 'TypedMessageManager - TypedMessage not found for id: "${msgId}".' if there is no TypedMessage
* in this.messages with an id equal to the passed msgId
* @fires An event with a name equal to `${msgId}:${status}`. The TypedMessage is also fired.
* @fires If status is 'rejected' or 'signed', an event with a name equal to `${msgId}:finished` is fired along
* with the TypedMessage
*
*/
_setMsgStatus (msgId, status) {
const msg = this.getMsg(msgId)
if (!msg) throw new Error('TypedMessageManager - Message not found for id: "${msgId}".')
@ -105,6 +226,15 @@ module.exports = class TypedMessageManager extends EventEmitter {
}
}
/**
* Sets a TypedMessage in this.messages to the passed TypedMessage if the ids are equal. Then saves the
* unapprovedTypedMsgs index to storage via this._saveMsgList
*
* @private
* @param {msg} TypedMessage A TypedMessage that will replace an existing TypedMessage (with the same
* id) in this.messages
*
*/
_updateMsg (msg) {
const index = this.messages.findIndex((message) => message.id === msg.id)
if (index !== -1) {
@ -113,6 +243,13 @@ module.exports = class TypedMessageManager extends EventEmitter {
this._saveMsgList()
}
/**
* Saves the unapproved TypedMessages, and their count, to this.memStore
*
* @private
* @fires 'updateBadge'
*
*/
_saveMsgList () {
const unapprovedTypedMessages = this.getUnapprovedMsgs()
const unapprovedTypedMessagesCount = Object.keys(unapprovedTypedMessages).length

View File

@ -263,6 +263,7 @@ module.exports = class MetamaskController extends EventEmitter {
/**
* Constructor helper: initialize a public config store.
* This store is used to make some config info available to Dapps synchronously.
*/
initPublicConfigStore () {
// get init state
@ -308,15 +309,16 @@ module.exports = class MetamaskController extends EventEmitter {
lostAccounts: this.configManager.getLostAccounts(),
seedWords: this.configManager.getSeedWords(),
forgottenPassword: this.configManager.getPasswordForgotten(),
isRevealingSeedWords: Boolean(this.configManager.getIsRevealingSeedWords()),
},
}
}
/**
* Returns an api-object which is consumed by the UI
* Returns an Object containing API Callback Functions.
* These functions are the interface for the UI.
* The API object can be transmitted over a stream with dnode.
*
* @returns {Object}
* @returns {Object} Object containing API functions.
*/
getApi () {
const keyringController = this.keyringController
@ -348,7 +350,6 @@ module.exports = class MetamaskController extends EventEmitter {
clearSeedWordCache: this.clearSeedWordCache.bind(this),
resetAccount: nodeify(this.resetAccount, this),
importAccountWithStrategy: this.importAccountWithStrategy.bind(this),
setIsRevealingSeedWords: this.configManager.setIsRevealingSeedWords.bind(this.configManager),
// vault management
submitPassword: nodeify(keyringController.submitPassword, keyringController),
@ -381,6 +382,7 @@ module.exports = class MetamaskController extends EventEmitter {
updateTransaction: nodeify(txController.updateTransaction, txController),
updateAndApproveTransaction: nodeify(txController.updateAndApproveTransaction, txController),
retryTransaction: nodeify(this.retryTransaction, this),
isNonceTaken: nodeify(txController.isNonceTaken, txController),
// messageManager
signMessage: nodeify(this.signMessage, this),
@ -407,16 +409,18 @@ module.exports = class MetamaskController extends EventEmitter {
//=============================================================================
/**
* Creates a new Vault(?) and create a new keychain(?)
* Creates a new Vault and create a new keychain.
*
* A vault is ...
* A vault, or KeyringController, is a controller that contains
* many different account strategies, currently called Keyrings.
* Creating it new means wiping all previous keyrings.
*
* A keychain is ...
* A keychain, or keyring, controls many accounts with a single backup and signing strategy.
* For example, a mnemonic phrase can generate many accounts, and is a keyring.
*
* @param {string} password
*
* @param {} password
*
* @returns {} vault
* @returns {Object} vault
*/
async createNewVaultAndKeychain (password) {
const release = await this.createVaultMutex.acquire()
@ -442,7 +446,7 @@ module.exports = class MetamaskController extends EventEmitter {
}
/**
* Create a new Vault and restore an existent keychain
* Create a new Vault and restore an existent keyring.
* @param {} password
* @param {} seed
*/
@ -459,11 +463,17 @@ module.exports = class MetamaskController extends EventEmitter {
}
}
/**
* @type Identity
* @property {string} name - The account nickname.
* @property {string} address - The account's ethereum address, in lower case.
* @property {boolean} mayBeFauceting - Whether this account is currently
* receiving funds from our automatic Ropsten faucet.
*/
/**
* Retrieves the first Identiy from the passed Vault and selects the related address
*
* An Identity is ...
*
* @param {} vault
*/
selectFirstIdentity (vault) {
@ -472,12 +482,12 @@ module.exports = class MetamaskController extends EventEmitter {
this.preferencesController.setSelectedAddress(address)
}
// ?
// Opinionated Keyring Management
//
// Account Management
//
/**
* Adds a new account to ...
* Adds a new account to the default (first) HD seed phrase Keyring.
*
* @returns {} keyState
*/
@ -507,6 +517,8 @@ module.exports = class MetamaskController extends EventEmitter {
*
* Used when creating a first vault, to allow confirmation.
* Also used when revealing the seed words in the confirmation view.
*
* @param {Function} cb - A callback called on completion.
*/
placeSeedWords (cb) {
@ -526,6 +538,8 @@ module.exports = class MetamaskController extends EventEmitter {
* Validity: seed phrase restores the accounts belonging to the current vault.
*
* Called when the first account is created and on unlocking the vault.
*
* @returns {Promise<string>} Seed phrase to be confirmed by the user.
*/
async verifySeedPhrase () {
@ -556,6 +570,7 @@ module.exports = class MetamaskController extends EventEmitter {
*
* The seed phrase remains available in the background process.
*
* @param {function} cb Callback function called with the current address.
*/
clearSeedWordCache (cb) {
this.configManager.setSeedWords(null)
@ -563,9 +578,13 @@ module.exports = class MetamaskController extends EventEmitter {
}
/**
* ?
* Clears the transaction history, to allow users to force-reset their nonces.
* Mostly used in development environments, when networks are restarted with
* the same network ID.
*
* @returns Promise<string> The current selected address.
*/
async resetAccount (cb) {
async resetAccount () {
const selectedAddress = this.preferencesController.getSelectedAddress()
this.txController.wipeTransactions(selectedAddress)
@ -577,11 +596,13 @@ module.exports = class MetamaskController extends EventEmitter {
}
/**
* Imports an account ... ?
* Imports an account with the specified import strategy.
* These are defined in app/scripts/account-import-strategies
* Each strategy represents a different way of serializing an Ethereum key pair.
*
* @param {} strategy
* @param {} args
* @param {} cb
* @param {string} strategy - A unique identifier for an account import strategy.
* @param {any} args - The data required by that strategy to import an account.
* @param {Function} cb - A callback function called with a state update on success.
*/
importAccountWithStrategy (strategy, args, cb) {
accountImporter.importAccount(strategy, args)
@ -595,13 +616,42 @@ module.exports = class MetamaskController extends EventEmitter {
}
// ---------------------------------------------------------------------------
// Identity Management (sign)
// Identity Management (signature operations)
// eth_sign methods:
/**
* @param {} msgParams
* @param {} cb
* Called when a Dapp uses the eth_sign method, to request user approval.
* eth_sign is a pure signature of arbitrary data. It is on a deprecation
* path, since this data can be a transaction, or can leak private key
* information.
*
* @param {Object} msgParams - The params passed to eth_sign.
* @param {Function} cb = The callback function called with the signature.
*/
signMessage (msgParams, cb) {
newUnsignedMessage (msgParams, cb) {
const msgId = this.messageManager.addUnapprovedMessage(msgParams)
this.sendUpdate()
this.opts.showUnconfirmedMessage()
this.messageManager.once(`${msgId}:finished`, (data) => {
switch (data.status) {
case 'signed':
return cb(null, data.rawSig)
case 'rejected':
return cb(new Error('MetaMask Message Signature: User denied message signature.'))
default:
return cb(new Error(`MetaMask Message Signature: Unknown problem: ${JSON.stringify(msgParams)}`))
}
})
}
/**
* Signifies user intent to complete an eth_sign method.
*
* @param {Object} msgParams The params passed to eth_call.
* @returns {Promise<Object>} Full state update.
*/
signMessage (msgParams) {
log.info('MetaMaskController - signMessage')
const msgId = msgParams.metamaskId
@ -620,148 +670,32 @@ module.exports = class MetamaskController extends EventEmitter {
})
}
// Prefixed Style Message Signing Methods:
/**
* Used to cancel a message submitted via eth_sign.
*
* @param {} msgParams
* @param {} cb
* @param {string} msgId - The id of the message to cancel.
*/
approvePersonalMessage (msgParams, cb) {
const msgId = this.personalMessageManager.addUnapprovedMessage(msgParams)
this.sendUpdate()
this.opts.showUnconfirmedMessage()
this.personalMessageManager.once(`${msgId}:finished`, (data) => {
switch (data.status) {
case 'signed':
return cb(null, data.rawSig)
case 'rejected':
return cb(new Error('MetaMask Message Signature: User denied transaction signature.'))
default:
return cb(new Error(`MetaMask Message Signature: Unknown problem: ${JSON.stringify(msgParams)}`))
}
})
}
/**
* @param {} msgParams
*/
signPersonalMessage (msgParams) {
log.info('MetaMaskController - signPersonalMessage')
const msgId = msgParams.metamaskId
// sets the status op the message to 'approved'
// and removes the metamaskId for signing
return this.personalMessageManager.approveMessage(msgParams)
.then((cleanMsgParams) => {
// signs the message
return this.keyringController.signPersonalMessage(cleanMsgParams)
})
.then((rawSig) => {
// tells the listener that the message has been signed
// and can be returned to the dapp
this.personalMessageManager.setMsgStatusSigned(msgId, rawSig)
return this.getState()
})
}
/**
* @param {} msgParams
*/
signTypedMessage (msgParams) {
log.info('MetaMaskController - signTypedMessage')
const msgId = msgParams.metamaskId
// sets the status op the message to 'approved'
// and removes the metamaskId for signing
return this.typedMessageManager.approveMessage(msgParams)
.then((cleanMsgParams) => {
// signs the message
return this.keyringController.signTypedMessage(cleanMsgParams)
})
.then((rawSig) => {
// tells the listener that the message has been signed
// and can be returned to the dapp
this.typedMessageManager.setMsgStatusSigned(msgId, rawSig)
return this.getState()
})
}
// ---------------------------------------------------------------------------
// Account Restauration
/**
* ?
*
* @param {} migratorOutput
*/
restoreOldVaultAccounts (migratorOutput) {
const { serialized } = migratorOutput
return this.keyringController.restoreKeyring(serialized)
.then(() => migratorOutput)
}
/**
* ?
*
* @param {} migratorOutput
*/
restoreOldLostAccounts (migratorOutput) {
const { lostAccounts } = migratorOutput
if (lostAccounts) {
this.configManager.setLostAccounts(lostAccounts.map(acct => acct.address))
return this.importLostAccounts(migratorOutput)
cancelMessage (msgId, cb) {
const messageManager = this.messageManager
messageManager.rejectMsg(msgId)
if (cb && typeof cb === 'function') {
cb(null, this.getState())
}
return Promise.resolve(migratorOutput)
}
// personal_sign methods:
/**
* Import (lost) Accounts
* Called when a dapp uses the personal_sign method.
* This is identical to the Geth eth_sign method, and may eventually replace
* eth_sign.
*
* @param {Object} {lostAccounts} @Array accounts <{ address, privateKey }>
* We currently define our eth_sign and personal_sign mostly for legacy Dapps.
*
* Uses the array's private keys to create a new Simple Key Pair keychain
* and add it to the keyring controller.
* @param {Object} msgParams - The params of the message to sign & return to the Dapp.
* @param {Function} cb - The callback function called with the signature.
* Passed back to the requesting Dapp.
*/
importLostAccounts ({ lostAccounts }) {
const privKeys = lostAccounts.map(acct => acct.privateKey)
return this.keyringController.restoreKeyring({
type: 'Simple Key Pair',
data: privKeys,
})
}
//=============================================================================
// END (VAULT / KEYRING RELATED METHODS)
//=============================================================================
//
//=============================================================================
// MESSAGES
//=============================================================================
async retryTransaction (txId, cb) {
await this.txController.retryTransaction(txId)
const state = await this.getState()
return state
}
newUnsignedMessage (msgParams, cb) {
const msgId = this.messageManager.addUnapprovedMessage(msgParams)
this.sendUpdate()
this.opts.showUnconfirmedMessage()
this.messageManager.once(`${msgId}:finished`, (data) => {
switch (data.status) {
case 'signed':
return cb(null, data.rawSig)
case 'rejected':
return cb(new Error('MetaMask Message Signature: User denied message signature.'))
default:
return cb(new Error(`MetaMask Message Signature: Unknown problem: ${JSON.stringify(msgParams)}`))
}
})
}
newUnsignedPersonalMessage (msgParams, cb) {
if (!msgParams.from) {
return cb(new Error('MetaMask Message Signature: from field is required.'))
@ -782,6 +716,52 @@ module.exports = class MetamaskController extends EventEmitter {
})
}
/**
* Signifies a user's approval to sign a personal_sign message in queue.
* Triggers signing, and the callback function from newUnsignedPersonalMessage.
*
* @param {Object} msgParams - The params of the message to sign & return to the Dapp.
* @returns {Promise<Object>} - A full state update.
*/
signPersonalMessage (msgParams) {
log.info('MetaMaskController - signPersonalMessage')
const msgId = msgParams.metamaskId
// sets the status op the message to 'approved'
// and removes the metamaskId for signing
return this.personalMessageManager.approveMessage(msgParams)
.then((cleanMsgParams) => {
// signs the message
return this.keyringController.signPersonalMessage(cleanMsgParams)
})
.then((rawSig) => {
// tells the listener that the message has been signed
// and can be returned to the dapp
this.personalMessageManager.setMsgStatusSigned(msgId, rawSig)
return this.getState()
})
}
/**
* Used to cancel a personal_sign type message.
* @param {string} msgId - The ID of the message to cancel.
* @param {Function} cb - The callback function called with a full state update.
*/
cancelPersonalMessage (msgId, cb) {
const messageManager = this.personalMessageManager
messageManager.rejectMsg(msgId)
if (cb && typeof cb === 'function') {
cb(null, this.getState())
}
}
// eth_signTypedData methods
/**
* Called when a dapp uses the eth_signTypedData method, per EIP 712.
*
* @param {Object} msgParams - The params passed to eth_signTypedData.
* @param {Function} cb - The callback function, called with the signature.
*/
newUnsignedTypedMessage (msgParams, cb) {
let msgId
try {
@ -804,22 +784,36 @@ module.exports = class MetamaskController extends EventEmitter {
})
}
cancelMessage (msgId, cb) {
const messageManager = this.messageManager
messageManager.rejectMsg(msgId)
if (cb && typeof cb === 'function') {
cb(null, this.getState())
}
}
cancelPersonalMessage (msgId, cb) {
const messageManager = this.personalMessageManager
messageManager.rejectMsg(msgId)
if (cb && typeof cb === 'function') {
cb(null, this.getState())
}
/**
* The method for a user approving a call to eth_signTypedData, per EIP 712.
* Triggers the callback in newUnsignedTypedMessage.
*
* @param {Object} msgParams - The params passed to eth_signTypedData.
* @returns {Object} Full state update.
*/
signTypedMessage (msgParams) {
log.info('MetaMaskController - signTypedMessage')
const msgId = msgParams.metamaskId
// sets the status op the message to 'approved'
// and removes the metamaskId for signing
return this.typedMessageManager.approveMessage(msgParams)
.then((cleanMsgParams) => {
// signs the message
return this.keyringController.signTypedMessage(cleanMsgParams)
})
.then((rawSig) => {
// tells the listener that the message has been signed
// and can be returned to the dapp
this.typedMessageManager.setMsgStatusSigned(msgId, rawSig)
return this.getState()
})
}
/**
* Used to cancel a eth_signTypedData type message.
* @param {string} msgId - The ID of the message to cancel.
* @param {Function} cb - The callback function called with a full state update.
*/
cancelTypedMessage (msgId, cb) {
const messageManager = this.typedMessageManager
messageManager.rejectMsg(msgId)
@ -828,18 +822,119 @@ module.exports = class MetamaskController extends EventEmitter {
}
}
// ---------------------------------------------------------------------------
// MetaMask Version 3 Migration Account Restauration Methods
/**
* A legacy method (probably dead code) that was used when we swapped out our
* key management library that we depended on.
*
* Described in:
* https://medium.com/metamask/metamask-3-migration-guide-914b79533cdd
*
* @deprecated
* @param {} migratorOutput
*/
restoreOldVaultAccounts (migratorOutput) {
const { serialized } = migratorOutput
return this.keyringController.restoreKeyring(serialized)
.then(() => migratorOutput)
}
/**
* A legacy method used to record user confirmation that they understand
* that some of their accounts have been recovered but should be backed up.
*
* @deprecated
* @param {Function} cb - A callback function called with a full state update.
*/
markAccountsFound (cb) {
this.configManager.setLostAccounts([])
this.sendUpdate()
cb(null, this.getState())
}
/**
* A legacy method (probably dead code) that was used when we swapped out our
* key management library that we depended on.
*
* Described in:
* https://medium.com/metamask/metamask-3-migration-guide-914b79533cdd
*
* @deprecated
* @param {} migratorOutput
*/
restoreOldLostAccounts (migratorOutput) {
const { lostAccounts } = migratorOutput
if (lostAccounts) {
this.configManager.setLostAccounts(lostAccounts.map(acct => acct.address))
return this.importLostAccounts(migratorOutput)
}
return Promise.resolve(migratorOutput)
}
/**
* An account object
* @typedef Account
* @property string privateKey - The private key of the account.
*/
/**
* Probably no longer needed, related to the Version 3 migration.
* Imports a hash of accounts to private keys into the vault.
*
* Described in:
* https://medium.com/metamask/metamask-3-migration-guide-914b79533cdd
*
* Uses the array's private keys to create a new Simple Key Pair keychain
* and add it to the keyring controller.
* @deprecated
* @param {Account[]} lostAccounts -
* @returns {Keyring[]} An array of the restored keyrings.
*/
importLostAccounts ({ lostAccounts }) {
const privKeys = lostAccounts.map(acct => acct.privateKey)
return this.keyringController.restoreKeyring({
type: 'Simple Key Pair',
data: privKeys,
})
}
//=============================================================================
// END (VAULT / KEYRING RELATED METHODS)
//=============================================================================
/**
* Allows a user to try to speed up a transaction by retrying it
* with higher gas.
*
* @param {string} txId - The ID of the transaction to speed up.
* @param {Function} cb - The callback function called with a full state update.
*/
async retryTransaction (txId, cb) {
await this.txController.retryTransaction(txId)
const state = await this.getState()
return state
}
//=============================================================================
// PASSWORD MANAGEMENT
//=============================================================================
/**
* Allows a user to begin the seed phrase recovery process.
* @param {Function} cb - A callback function called when complete.
*/
markPasswordForgotten(cb) {
this.configManager.setPasswordForgotten(true)
this.sendUpdate()
cb()
}
/**
* Allows a user to end the seed phrase recovery process.
* @param {Function} cb - A callback function called when complete.
*/
unMarkPasswordForgotten(cb) {
this.configManager.setPasswordForgotten(false)
this.sendUpdate()
@ -850,6 +945,13 @@ module.exports = class MetamaskController extends EventEmitter {
// SETUP
//=============================================================================
/**
* Used to create a multiplexed stream for connecting to an untrusted context
* like a Dapp or other extension.
* @param {*} connectionStream - The Duplex stream to connect to.
* @param {string} originDomain - The domain requesting the stream, which
* may trigger a blacklist reload.
*/
setupUntrustedCommunication (connectionStream, originDomain) {
// Check if new connection is blacklisted
if (this.blacklistController.checkForPhishing(originDomain)) {
@ -865,6 +967,16 @@ module.exports = class MetamaskController extends EventEmitter {
this.setupPublicConfig(mux.createStream('publicConfig'))
}
/**
* Used to create a multiplexed stream for connecting to a trusted context,
* like our own user interfaces, which have the provider APIs, but also
* receive the exported API from this controller, which includes trusted
* functions, like the ability to approve transactions or sign messages.
*
* @param {*} connectionStream - The duplex stream to connect to.
* @param {string} originDomain - The domain requesting the connection,
* used in logging and error reporting.
*/
setupTrustedCommunication (connectionStream, originDomain) {
// setup multiplexing
const mux = setupMultiplex(connectionStream)
@ -873,12 +985,25 @@ module.exports = class MetamaskController extends EventEmitter {
this.setupProviderConnection(mux.createStream('provider'), originDomain)
}
/**
* Called when we detect a suspicious domain. Requests the browser redirects
* to our anti-phishing page.
*
* @private
* @param {*} connectionStream - The duplex stream to the per-page script,
* for sending the reload attempt to.
* @param {string} hostname - The URL that triggered the suspicion.
*/
sendPhishingWarning (connectionStream, hostname) {
const mux = setupMultiplex(connectionStream)
const phishingStream = mux.createStream('phishing')
phishingStream.write({ hostname })
}
/**
* A method for providing our API over a stream using Dnode.
* @param {*} outStream - The stream to provide our API over.
*/
setupControllerConnection (outStream) {
const api = this.getApi()
const dnode = Dnode(api)
@ -897,6 +1022,11 @@ module.exports = class MetamaskController extends EventEmitter {
})
}
/**
* A method for serving our ethereum provider over a given stream.
* @param {*} outStream - The stream to provide over.
* @param {string} origin - The URI of the requesting resource.
*/
setupProviderConnection (outStream, origin) {
// setup json rpc engine stack
const engine = new RpcEngine()
@ -926,6 +1056,16 @@ module.exports = class MetamaskController extends EventEmitter {
)
}
/**
* 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.
*
* Since synchronous methods have been deprecated in web3,
* this is a good candidate for deprecation.
*
* @param {*} outStream - The stream to provide public config over.
*/
setupPublicConfig (outStream) {
pump(
asStream(this.publicConfigStore),
@ -936,10 +1076,21 @@ module.exports = class MetamaskController extends EventEmitter {
)
}
/**
* A method for emitting the full MetaMask state to all registered listeners.
* @private
*/
privateSendUpdate () {
this.emit('update', this.getState())
}
/**
* A method for estimating a good gas price at recent prices.
* Returns the lowest price that would have been included in
* 50% of recent blocks.
*
* @returns {string} A hex representation of the suggested wei gas price.
*/
getGasPrice () {
const { recentBlocksController } = this
const { recentBlocks } = recentBlocksController.store.getState()
@ -973,6 +1124,11 @@ module.exports = class MetamaskController extends EventEmitter {
// Log blocks
/**
* A method for setting the user's preferred display currency.
* @param {string} currencyCode - The code of the preferred currency.
* @param {Function} cb - A callback function returning currency info.
*/
setCurrentCurrency (currencyCode, cb) {
try {
this.currencyController.setCurrentCurrency(currencyCode)
@ -988,6 +1144,13 @@ module.exports = class MetamaskController extends EventEmitter {
}
}
/**
* A method for forwarding the user to the easiest way to obtain ether,
* or the network "gas" currency, for the current selected network.
*
* @param {string} address - The address to fund.
* @param {string} amount - The amount of ether desired, as a base 10 string.
*/
buyEth (address, amount) {
if (!amount) amount = '5'
const network = this.networkController.getNetworkState()
@ -995,18 +1158,33 @@ module.exports = class MetamaskController extends EventEmitter {
if (url) this.platform.openWindow({ url })
}
/**
* A method for triggering a shapeshift currency transfer.
* @param {string} depositAddress - The address to deposit to.
* @property {string} depositType - An abbreviation of the type of crypto currency to be deposited.
*/
createShapeShiftTx (depositAddress, depositType) {
this.shapeshiftController.createShapeShiftTx(depositAddress, depositType)
}
// network
async setCustomRpc (rpcTarget, rpcList) {
/**
* A method for selecting a custom URL for an ethereum RPC provider.
* @param {string} rpcTarget - A URL for a valid Ethereum RPC API.
* @returns {Promise<String>} - The RPC Target URL confirmed.
*/
async setCustomRpc (rpcTarget) {
this.networkController.setRpcTarget(rpcTarget)
await this.preferencesController.updateFrequentRpcList(rpcTarget)
return rpcTarget
}
/**
* Sets whether or not to use the blockie identicon format.
* @param {boolean} val - True for bockie, false for jazzicon.
* @param {Function} cb - A callback function called when complete.
*/
setUseBlockie (val, cb) {
try {
this.preferencesController.setUseBlockie(val)
@ -1016,6 +1194,11 @@ module.exports = class MetamaskController extends EventEmitter {
}
}
/**
* A method for setting a user's current locale, affecting the language rendered.
* @param {string} key - Locale identifier.
* @param {Function} cb - A callback function called when complete.
*/
setCurrentLocale (key, cb) {
try {
this.preferencesController.setCurrentLocale(key)
@ -1025,6 +1208,11 @@ module.exports = class MetamaskController extends EventEmitter {
}
}
/**
* A method for initializing storage the first time.
* @param {Object} initState - The default state to initialize with.
* @private
*/
recordFirstTimeInfo (initState) {
if (!('firstTimeInfo' in initState)) {
initState.firstTimeInfo = {
@ -1034,11 +1222,21 @@ module.exports = class MetamaskController extends EventEmitter {
}
}
/**
* A method for recording whether the MetaMask user interface is open or not.
* @private
* @param {boolean} open
*/
set isClientOpen (open) {
this._isClientOpen = open
this.isClientOpenAndUnlocked = this.getState().isUnlocked && open
}
/**
* A method for activating the retrieval of price data, which should only be fetched when the UI is visible.
* @private
* @param {boolean} active - True if price data should be getting fetched.
*/
set isClientOpenAndUnlocked (active) {
this.tokenRatesController.isActive = active
}

View File

@ -7,7 +7,7 @@ This migration updates "transaction state history" to diffs style
*/
const clone = require('clone')
const txStateHistoryHelper = require('../lib/tx-state-history-helper')
const txStateHistoryHelper = require('../controllers/transactions/lib/tx-state-history-helper')
module.exports = {

View File

@ -0,0 +1,49 @@
const fs = require('fs')
const { SourceMapConsumer } = require('source-map')
//
// Utility to help check if sourcemaps are working
//
// searches `dist/chrome/inpage.js` for "new Error" statements
// and prints their source lines using the sourcemaps.
// if not working it may error or print minified garbage
//
start()
async function start() {
const rawBuild = fs.readFileSync(__dirname + '/../dist/chrome/inpage.js', 'utf8')
const rawSourceMap = fs.readFileSync(__dirname + '/../dist/sourcemaps/inpage.js.map', 'utf8')
const consumer = await new SourceMapConsumer(rawSourceMap)
console.log('hasContentsOfAllSources:', consumer.hasContentsOfAllSources(), '\n')
console.log('sources:')
consumer.sources.map((sourcePath) => console.log(sourcePath))
console.log('\nexamining "new Error" statements:\n')
const sourceLines = rawBuild.split('\n')
sourceLines.map(line => indicesOf('new Error', line))
.forEach((errorIndices, lineIndex) => {
// if (errorIndex === null) return console.log('line does not contain "new Error"')
errorIndices.forEach((errorIndex) => {
const position = { line: lineIndex + 1, column: errorIndex }
const result = consumer.originalPositionFor(position)
if (!result.source) return console.warn(`!! missing source for position: ${position}`)
// filter out deps distributed minified without sourcemaps
if (result.source === 'node_modules/browserify/node_modules/browser-pack/_prelude.js') return // minified mess
if (result.source === 'node_modules/web3/dist/web3.min.js') return // minified mess
const sourceContent = consumer.sourceContentFor(result.source)
const sourceLines = sourceContent.split('\n')
const line = sourceLines[result.line-1]
console.log(`\n========================== ${result.source} ====================================\n`)
console.log(line)
console.log(`\n==============================================================================\n`)
})
})
}
function indicesOf(substring, string) {
var a=[],i=-1;
while((i=string.indexOf(substring,i+1)) >= 0) a.push(i);
return a;
}

BIN
docs/transaction-flow.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 138 KiB

View File

@ -247,7 +247,7 @@ gulp.task('dev:scss', createScssBuildTask({
src: 'ui/app/css/index.scss',
dest: 'ui/app/css/output',
devMode: true,
pattern: 'ui/app/css/**/*.scss',
pattern: 'ui/app/**/*.scss',
}))
function createScssBuildTask({ src, dest, devMode, pattern }) {
@ -484,16 +484,6 @@ function generateBundler(opts, performBundle) {
NODE_ENV: opts.devMode ? 'development' : 'production',
}))
// Minification
if (opts.minifyBuild) {
bundler.transform('uglifyify', {
global: true,
mangle: {
reserved: [ 'MetamaskInpageProvider' ]
},
})
}
if (opts.watch) {
bundler = watchify(bundler)
// on any file update, re-runs the bundler
@ -567,6 +557,16 @@ function bundleTask(opts) {
.pipe(sourcemaps.init({ loadMaps: true }))
}
// Minification
if (opts.minifyBuild) {
buildStream = buildStream
.pipe(uglify({
mangle: {
reserved: [ 'MetamaskInpageProvider' ]
},
}))
}
// Finalize Source Maps (writes .map file)
if (opts.buildSourceMaps) {
buildStream = buildStream

View File

@ -70,10 +70,14 @@ class ImportAccountScreen extends Component {
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)
}
}

View File

@ -8,7 +8,6 @@ import Identicon from '../../../../ui/app/components/identicon'
import Breadcrumbs from './breadcrumbs'
import LoadingScreen from './loading-screen'
import { DEFAULT_ROUTE, INITIALIZE_CONFIRM_SEED_ROUTE } from '../../../../ui/app/routes'
import { confirmSeedWords } from '../../../../ui/app/actions'
const LockIcon = props => (
<svg
@ -45,8 +44,6 @@ class BackupPhraseScreen extends Component {
address: PropTypes.string.isRequired,
seedWords: PropTypes.string,
history: PropTypes.object,
isRevealingSeedWords: PropTypes.bool,
clearSeedWords: PropTypes.func,
};
static defaultProps = {
@ -61,14 +58,6 @@ class BackupPhraseScreen extends Component {
}
componentWillMount () {
this.checkSeedWords()
}
componentDidUpdate () {
this.checkSeedWords()
}
checkSeedWords () {
const { seedWords, history } = this.props
if (!seedWords) {
@ -103,29 +92,9 @@ class BackupPhraseScreen extends Component {
)
}
renderSubmitButton () {
const { isRevealingSeedWords, clearSeedWords, history } = this.props
const { isShowingSecret } = this.state
return isRevealingSeedWords
? <button
className="first-time-flow__button"
onClick={() => clearSeedWords().then(() => history.push(DEFAULT_ROUTE))}
disabled={!isShowingSecret}
>
Done
</button>
: <button
className="first-time-flow__button"
onClick={() => isShowingSecret && history.push(INITIALIZE_CONFIRM_SEED_ROUTE)}
disabled={!isShowingSecret}
>
Next
</button>
}
renderSecretScreen () {
const { isRevealingSeedWords } = this.props
const { isShowingSecret } = this.state
const { history } = this.props
return (
<div className="backup-phrase__content-wrapper">
@ -152,8 +121,14 @@ class BackupPhraseScreen extends Component {
</div>
</div>
<div className="backup-phrase__next-button">
{ this.renderSubmitButton() }
{ !isRevealingSeedWords && <Breadcrumbs total={3} currentIndex={1} />}
<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>
)
@ -175,25 +150,13 @@ class BackupPhraseScreen extends Component {
}
}
const mapStateToProps = ({ metamask, appState }) => {
const { selectedAddress, seedWords, isRevealingSeedWords } = metamask
const { isLoading } = appState
return {
seedWords,
isRevealingSeedWords,
isLoading,
address: selectedAddress,
}
}
const mapDispatchToProps = dispatch => {
return {
clearSeedWords: () => dispatch(confirmSeedWords()),
}
}
export default compose(
withRouter,
connect(mapStateToProps, mapDispatchToProps),
connect(
({ metamask: { selectedAddress, seedWords }, appState: { isLoading } }) => ({
seedWords,
isLoading,
address: selectedAddress,
})
)
)(BackupPhraseScreen)

View File

@ -1,6 +1,6 @@
MetaMask is beta software.
When you log in to MetaMask, your current account is visible to every new site you visit.
When you log in to MetaMask, your current account's address is visible to every new site you visit. This can be used to look up your account balances of Ether and other tokens.
For your privacy, for now, please sign out of MetaMask when you're done using a site.

File diff suppressed because one or more lines are too long

View File

@ -96,6 +96,8 @@ class JsonImportSubview extends Component {
}
this.props.importNewAccount([ fileContents, password ])
// JS runtime requires caught rejections but failures are handled by Redux
.catch()
}
}

View File

@ -64,4 +64,6 @@ PrivateKeyImportView.prototype.createNewKeychain = function () {
const input = document.getElementById('private-key-box')
const privateKey = input.value
this.props.dispatch(actions.importNewAccount('Private Key', [ privateKey ]))
// JS runtime requires caught rejections but failures are handled by Redux
.catch()
}

View File

@ -35,7 +35,7 @@ const HDCreateVaultComplete = require('./keychains/hd/create-vault-complete')
const HDRestoreVaultScreen = require('./keychains/hd/restore-vault')
const RevealSeedConfirmation = require('./keychains/hd/recover-seed/confirmation')
const AccountDropdowns = require('./components/account-dropdowns').AccountDropdowns
const { BETA_UI_NETWORK_TYPE } = require('../../app/scripts/config').enums
const { BETA_UI_NETWORK_TYPE } = require('../../app/scripts/controllers/network/enums')
module.exports = connect(mapStateToProps)(App)

View File

@ -8,7 +8,7 @@ const ShapeshiftForm = require('./shapeshift-form')
const Loading = require('./loading')
const AccountPanel = require('./account-panel')
const RadioList = require('./custom-radio-list')
const networkNames = require('../../../app/scripts/config.js').networkNames
const { getNetworkDisplayName } = require('../../../app/scripts/controllers/network/util')
module.exports = connect(mapStateToProps)(BuyButtonSubview)
@ -142,7 +142,7 @@ BuyButtonSubview.prototype.primarySubview = function () {
case '3':
case '4':
case '42':
const networkName = networkNames[network]
const networkName = getNetworkDisplayName(network)
const label = `${networkName} Test Faucet`
return (
h('div.flex-column', {

View File

@ -16,8 +16,7 @@ const addressSummary = util.addressSummary
const nameForAddress = require('../../lib/contract-namer')
const BNInput = require('./bn-as-decimal-input')
// corresponds with 0.1 GWEI
const MIN_GAS_PRICE_BN = new BN('100000000')
const MIN_GAS_PRICE_BN = new BN('0')
const MIN_GAS_LIMIT_BN = new BN('21000')
module.exports = PendingTx

View File

@ -138,7 +138,7 @@ ShapeshiftForm.prototype.renderMain = function () {
width: '229px',
height: '82px',
},
}, this.props.warning)
}, this.props.warning + '')
: this.renderInfo(),
this.renderRefundAddressForCoin(coin),

4506
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -76,6 +76,7 @@
"classnames": "^2.2.5",
"clone": "^2.1.1",
"copy-to-clipboard": "^3.0.8",
"css-loader": "^0.28.11",
"currency-formatter": "^1.4.2",
"debounce": "^1.0.0",
"debounce-stream": "^2.0.0",
@ -87,10 +88,9 @@
"ensnare": "^1.0.0",
"eslint-plugin-react": "^7.4.0",
"eth-bin-to-ops": "^1.0.1",
"eth-block-tracker": "^2.3.0",
"eth-contract-metadata": "^1.1.5",
"eth-hd-keyring": "^1.2.1",
"eth-json-rpc-filters": "^1.2.5",
"eth-json-rpc-filters": "^1.2.6",
"eth-json-rpc-infura": "^3.0.0",
"eth-keyring-controller": "^2.2.0",
"eth-phishing-detect": "^1.1.4",
@ -103,7 +103,7 @@
"ethereumjs-wallet": "^0.6.0",
"etherscan-link": "^1.0.2",
"ethjs": "^0.3.4",
"ethjs-contract": "^0.1.9",
"ethjs-contract": "^0.2.0",
"ethjs-ens": "^2.0.0",
"ethjs-query": "^0.3.4",
"express": "^4.15.5",
@ -111,6 +111,7 @@
"extensionizer": "^1.0.0",
"fast-json-patch": "^2.0.4",
"fast-levenshtein": "^2.0.6",
"file-loader": "^1.1.11",
"fuse.js": "^3.2.0",
"gulp": "github:gulpjs/gulp#4.0",
"gulp-autoprefixer": "^5.0.0",
@ -152,7 +153,7 @@
"pumpify": "^1.3.4",
"qrcode-npm": "0.0.3",
"ramda": "^0.24.1",
"raven-js": "^3.24.0",
"raven-js": "^3.24.2",
"react": "^15.6.2",
"react-addons-css-transition-group": "^15.6.0",
"react-dom": "^15.6.2",
@ -184,7 +185,7 @@
"valid-url": "^1.0.9",
"vreme": "^3.0.2",
"web3": "^0.20.1",
"web3-provider-engine": "^13.8.0",
"web3-provider-engine": "^14.0.5",
"web3-stream-provider": "^3.0.1",
"xtend": "^4.0.1"
},
@ -203,7 +204,7 @@
"brfs": "^1.4.3",
"browserify": "^16.1.1",
"chai": "^4.1.0",
"chromedriver": "^2.34.1",
"chromedriver": "2.36.0",
"compression": "^1.7.1",
"coveralls": "^3.0.0",
"cross-env": "^5.1.4",
@ -216,9 +217,10 @@
"eslint-plugin-json": "^1.2.0",
"eslint-plugin-mocha": "^5.0.0",
"eslint-plugin-react": "^7.4.0",
"eth-json-rpc-middleware": "^1.2.7",
"eth-json-rpc-middleware": "^1.6.0",
"fs-promise": "^2.0.3",
"ganache-cli": "^6.1.0",
"ganache-core": "^2.1.0",
"gifencoder": "^1.1.0",
"gulp": "github:gulpjs/gulp#6d71a658c61edb3090221579d8f97dbe086ba2ed",
"gulp-babel": "^7.0.0",
@ -253,8 +255,10 @@
"mocha-sinon": "^2.0.0",
"nock": "^9.0.14",
"node-sass": "^4.7.2",
"nsp": "^3.2.1",
"nyc": "^11.0.3",
"open": "0.0.5",
"path": "^0.12.7",
"png-file-stream": "^1.0.0",
"prompt": "^1.0.0",
"qs": "^6.2.0",
@ -264,20 +268,23 @@
"react-test-renderer": "^15.6.2",
"react-testutils-additions": "^15.2.0",
"redux-test-utils": "^0.2.2",
"resolve-url-loader": "^2.3.0",
"rimraf": "^2.6.2",
"sass-loader": "^7.0.1",
"selenium-webdriver": "^3.5.0",
"shell-parallel": "^1.0.3",
"sinon": "^5.0.0",
"source-map": "^0.7.2",
"style-loader": "^0.21.0",
"stylelint-config-standard": "^18.2.0",
"tape": "^4.5.1",
"testem": "^2.0.0",
"through2": "^2.0.3",
"uglifyify": "^4.0.5",
"vinyl-buffer": "^1.0.1",
"vinyl-source-stream": "^2.0.0",
"watchify": "^3.9.0"
},
"engines": {
"node": ">=0.8.0"
"node": ">=8.0.0"
}
}

View File

@ -23,6 +23,37 @@ global.ethQuery = {
global.ethereumProvider = {}
async function customizeGas (assert, price, limit, ethFee, usdFee) {
const sendGasOpenCustomizeModalButton = await queryAsync($, '.sliders-icon-container')
sendGasOpenCustomizeModalButton[0].click()
const customizeGasModal = await queryAsync($, '.send-v2__customize-gas')
assert.ok(customizeGasModal[0], 'should render the customize gas modal')
const customizeGasPriceInput = (await queryAsync($, '.send-v2__gas-modal-card')).first().find('input')
customizeGasPriceInput.val(price)
reactTriggerChange(customizeGasPriceInput[0])
const customizeGasLimitInput = (await queryAsync($, '.send-v2__gas-modal-card')).last().find('input')
customizeGasLimitInput.val(limit)
reactTriggerChange(customizeGasLimitInput[0])
const customizeGasSaveButton = await queryAsync($, '.send-v2__customize-gas__save')
customizeGasSaveButton[0].click()
const sendGasField = await queryAsync($, '.send-v2__gas-fee-display')
assert.equal(
(await findAsync(sendGasField, '.currency-display__input-wrapper > input')).val(),
ethFee,
'send gas field should show customized gas total'
)
assert.equal(
(await findAsync(sendGasField, '.currency-display__converted-value'))[0].textContent,
usdFee,
'send gas field should show customized gas total converted to USD'
)
}
async function runSendFlowTest(assert, done) {
console.log('*** start runSendFlowTest')
const selectState = await queryAsync($, 'select')
@ -95,32 +126,8 @@ async function runSendFlowTest(assert, done) {
'send gas field should show estimated gas total converted to USD'
)
const sendGasOpenCustomizeModalButton = await queryAsync($, '.sliders-icon-container')
sendGasOpenCustomizeModalButton[0].click()
const customizeGasModal = await queryAsync($, '.send-v2__customize-gas')
assert.ok(customizeGasModal[0], 'should render the customize gas modal')
const customizeGasPriceInput = (await queryAsync($, '.send-v2__gas-modal-card')).first().find('input')
customizeGasPriceInput.val(50)
reactTriggerChange(customizeGasPriceInput[0])
const customizeGasLimitInput = (await queryAsync($, '.send-v2__gas-modal-card')).last().find('input')
customizeGasLimitInput.val(60000)
reactTriggerChange(customizeGasLimitInput[0])
const customizeGasSaveButton = await queryAsync($, '.send-v2__customize-gas__save')
customizeGasSaveButton[0].click()
assert.equal(
(await findAsync(sendGasField, '.currency-display__input-wrapper > input')).val(),
'0.003',
'send gas field should show customized gas total'
)
assert.equal(
(await findAsync(sendGasField, '.currency-display__converted-value'))[0].textContent,
'$3.60 USD',
'send gas field should show customized gas total converted to USD'
)
await customizeGas(assert, 0, 21000, '0', '$0.00 USD')
await customizeGas(assert, 500, 60000, '0.003', '$3.60 USD')
const sendButton = await queryAsync($, 'button.btn-primary--lg.page-container__footer-button')
assert.equal(sendButton[0].textContent, 'Next', 'next button rendered')

View File

@ -1,14 +1,28 @@
const JsonRpcEngine = require('json-rpc-engine')
const scaffoldMiddleware = require('eth-json-rpc-middleware/scaffold')
const TestBlockchain = require('eth-block-tracker/test/util/testBlockMiddleware')
const providerAsMiddleware = require('eth-json-rpc-middleware/providerAsMiddleware')
const GanacheCore = require('ganache-core')
module.exports = {
createEngineForTestData,
providerFromEngine,
scaffoldMiddleware,
createTestProviderTools,
getTestSeed,
getTestAccounts,
}
function getTestSeed () {
return 'people carpet cluster attract ankle motor ozone mass dove original primary mask'
}
function getTestAccounts () {
return [
{ address: '0x88bb7F89eB5e5b30D3e15a57C68DBe03C6aCCB21', key: Buffer.from('254A8D551474F35CCC816388B4ED4D20B945C96B7EB857A68064CB9E9FB2C092', 'hex') },
{ address: '0x1fe9aAB565Be19629fF4e8541ca2102fb42D7724', key: Buffer.from('6BAB5A4F2A6911AF8EE2BD32C6C05F6643AC48EF6C939CDEAAAE6B1620805A9B', 'hex') },
{ address: '0xbda5c89aa6bA1b352194291AD6822C92AbC87c7B', key: Buffer.from('9B11D7F833648F26CE94D544855558D7053ECD396E4F4563968C232C012879B0', 'hex') },
]
}
function createEngineForTestData () {
return new JsonRpcEngine()
@ -21,11 +35,13 @@ function providerFromEngine (engine) {
function createTestProviderTools (opts = {}) {
const engine = createEngineForTestData()
const testBlockchain = new TestBlockchain()
// handle provided hooks
engine.push(scaffoldMiddleware(opts.scaffold || {}))
// handle block tracker methods
engine.push(testBlockchain.createMiddleware())
engine.push(providerAsMiddleware(GanacheCore.provider({
mnemonic: getTestSeed(),
})))
// wrap in standard provider interface
const provider = providerFromEngine(engine)
return { provider, engine, testBlockchain }
return { provider, engine }
}

View File

@ -8,6 +8,11 @@ const blacklistJSON = require('../stub/blacklist')
const firstTimeState = require('../../app/scripts/first-time-state')
const currentNetworkId = 42
const DEFAULT_LABEL = 'Account 1'
const TEST_SEED = 'debris dizzy just program just float decrease vacant alarm reduce speak stadium'
const TEST_ADDRESS = '0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc'
const TEST_SEED_ALT = 'setup olympic issue mobile velvet surge alcohol burger horse view reopen gentle'
const TEST_ADDRESS_ALT = '0xc42edfcc21ed14dda456aa0756c153f7985d8813'
describe('MetaMaskController', function () {
let metamaskController
@ -100,18 +105,29 @@ describe('MetaMaskController', function () {
describe('#createNewVaultAndRestore', function () {
it('should be able to call newVaultAndRestore despite a mistake.', async function () {
const password = 'what-what-what'
const wrongSeed = 'debris dizzy just program just float decrease vacant alarm reduce speak stadiu'
const rightSeed = 'debris dizzy just program just float decrease vacant alarm reduce speak stadium'
await metamaskController.createNewVaultAndRestore(password, wrongSeed)
.catch((e) => {
return
})
await metamaskController.createNewVaultAndRestore(password, rightSeed)
await metamaskController.createNewVaultAndRestore(password, TEST_SEED.slice(0, -1)).catch((e) => null)
await metamaskController.createNewVaultAndRestore(password, TEST_SEED)
assert(metamaskController.keyringController.createNewVaultAndRestore.calledTwice)
})
it('should clear previous identities after vault restoration', async () => {
await metamaskController.createNewVaultAndRestore('foobar1337', TEST_SEED)
assert.deepEqual(metamaskController.getState().identities, {
[TEST_ADDRESS]: { address: TEST_ADDRESS, name: DEFAULT_LABEL },
})
await metamaskController.keyringController.saveAccountLabel(TEST_ADDRESS, 'Account Foo')
assert.deepEqual(metamaskController.getState().identities, {
[TEST_ADDRESS]: { address: TEST_ADDRESS, name: 'Account Foo' },
})
await metamaskController.createNewVaultAndRestore('foobar1337', TEST_SEED_ALT)
assert.deepEqual(metamaskController.getState().identities, {
[TEST_ADDRESS_ALT]: { address: TEST_ADDRESS_ALT, name: DEFAULT_LABEL },
})
})
})
describe('#getApi', function () {

View File

@ -1,6 +1,10 @@
const assert = require('assert')
const nock = require('nock')
const NetworkController = require('../../app/scripts/controllers/network')
const {
getNetworkDisplayName,
getNetworkEndpoints,
} = require('../../app/scripts/controllers/network/util')
const { createTestProviderTools } = require('../stub/provider')
const providerResultStub = {}
@ -79,4 +83,40 @@ describe('# Network Controller', function () {
})
})
})
})
})
describe('# Network utils', () => {
it('getNetworkDisplayName should return the correct network name', () => {
const tests = [
{
input: 3,
expected: 'Ropsten',
}, {
input: 4,
expected: 'Rinkeby',
}, {
input: 42,
expected: 'Kovan',
}, {
input: 'ropsten',
expected: 'Ropsten',
}, {
input: 'rinkeby',
expected: 'Rinkeby',
}, {
input: 'kovan',
expected: 'Kovan',
}, {
input: 'mainnet',
expected: 'Main Ethereum Network',
},
]
tests.forEach(({ input, expected }) => assert.equal(getNetworkDisplayName(input), expected))
})
it('getNetworkEndpoints should return the correct endpoints', () => {
assert.equal(getNetworkEndpoints('networkBeta').ropsten, 'https://ropsten.infura.io/metamask2')
assert.equal(getNetworkEndpoints('network').rinkeby, 'https://rinkeby.infura.io/metamask')
})
})

View File

@ -1,5 +1,5 @@
const assert = require('assert')
const NonceTracker = require('../../app/scripts/lib/nonce-tracker')
const NonceTracker = require('../../app/scripts/controllers/transactions/nonce-tracker')
const MockTxGen = require('../lib/mock-tx-gen')
let providerResultStub = {}

View File

@ -4,7 +4,7 @@ const EthTx = require('ethereumjs-tx')
const ObservableStore = require('obs-store')
const clone = require('clone')
const { createTestProviderTools } = require('../stub/provider')
const PendingTransactionTracker = require('../../app/scripts/lib/pending-tx-tracker')
const PendingTransactionTracker = require('../../app/scripts/controllers/transactions/pending-tx-tracker')
const MockTxGen = require('../lib/mock-tx-gen')
const sinon = require('sinon')
const noop = () => true

View File

@ -5,17 +5,16 @@ const EthjsQuery = require('ethjs-query')
const ObservableStore = require('obs-store')
const sinon = require('sinon')
const TransactionController = require('../../app/scripts/controllers/transactions')
const TxGasUtils = require('../../app/scripts/lib/tx-gas-utils')
const { createTestProviderTools } = require('../stub/provider')
const TxGasUtils = require('../../app/scripts/controllers/transactions/tx-gas-utils')
const { createTestProviderTools, getTestAccounts } = require('../stub/provider')
const noop = () => true
const currentNetworkId = 42
const otherNetworkId = 36
const privKey = new Buffer('8718b9618a37d1fc78c436511fc6df3c8258d3250635bba617f33003270ec03e', 'hex')
describe('Transaction Controller', function () {
let txController, provider, providerResultStub, testBlockchain
let txController, provider, providerResultStub, query, fromAccount
beforeEach(function () {
providerResultStub = {
@ -24,9 +23,9 @@ describe('Transaction Controller', function () {
// by default, all accounts are external accounts (not contracts)
eth_getCode: '0x',
}
const providerTools = createTestProviderTools({ scaffold: providerResultStub })
provider = providerTools.provider
testBlockchain = providerTools.testBlockchain
provider = createTestProviderTools({ scaffold: providerResultStub }).provider
query = new EthjsQuery(provider)
fromAccount = getTestAccounts()[0]
txController = new TransactionController({
provider,
@ -34,13 +33,43 @@ describe('Transaction Controller', function () {
txHistoryLimit: 10,
blockTracker: { getCurrentBlock: noop, on: noop, once: noop },
signTransaction: (ethTx) => new Promise((resolve) => {
ethTx.sign(privKey)
ethTx.sign(fromAccount.key)
resolve()
}),
})
txController.nonceTracker.getNonceLock = () => Promise.resolve({ nextNonce: 0, releaseLock: noop })
})
describe('#isNonceTaken', function () {
it('should return true', function (done) {
txController.txStateManager._saveTxList([
{ id: 1, status: 'submitted', metamaskNetworkId: currentNetworkId, txParams: {nonce: 0, from: '0x8ACCE2391C0d510a6C5E5D8f819A678F79B7E675'} },
{ id: 2, status: 'confirmed', metamaskNetworkId: currentNetworkId, txParams: {nonce: 0, from: '0x8ACCE2391C0d510a6C5E5D8f819A678F79B7E675'} },
{ id: 3, status: 'submitted', metamaskNetworkId: currentNetworkId, txParams: {nonce: 0, from: '0x8ACCE2391C0d510a6C5E5D8f819A678F79B7E675'} },
])
txController.isNonceTaken({txParams: {nonce:0, from:'0x8ACCE2391C0d510a6C5E5D8f819A678F79B7E675'}})
.then((isNonceTaken) => {
assert(isNonceTaken)
done()
}).catch(done)
})
it('should return false', function (done) {
txController.txStateManager._saveTxList([
{ id: 1, status: 'submitted', metamaskNetworkId: currentNetworkId, txParams: {nonce: 0, from: '0x8ACCE2391C0d510a6C5E5D8f819A678F79B7E675'} },
{ id: 2, status: 'submitted', metamaskNetworkId: currentNetworkId, txParams: {nonce: 0, from: '0x8ACCE2391C0d510a6C5E5D8f819A678F79B7E675'} },
{ id: 3, status: 'submitted', metamaskNetworkId: currentNetworkId, txParams: {nonce: 0, from: '0x8ACCE2391C0d510a6C5E5D8f819A678F79B7E675'} },
])
txController.isNonceTaken({txParams: {nonce:0, from:'0x8ACCE2391C0d510a6C5E5D8f819A678F79B7E675'}})
.then((isNonceTaken) => {
assert(!isNonceTaken)
done()
}).catch(done)
})
})
describe('#getState', function () {
it('should return a state object with the right keys and datat types', function () {
const exposedState = txController.getState()
@ -188,7 +217,7 @@ describe('Transaction Controller', function () {
})
describe('#addTxDefaults', function () {
describe('#addTxGasDefaults', function () {
it('should add the tx defaults if their are none', function (done) {
const txMeta = {
'txParams': {
@ -199,7 +228,7 @@ describe('Transaction Controller', function () {
providerResultStub.eth_gasPrice = '4a817c800'
providerResultStub.eth_getBlockByNumber = { gasLimit: '47b784' }
providerResultStub.eth_estimateGas = '5209'
txController.addTxDefaults(txMeta)
txController.addTxGasDefaults(txMeta)
.then((txMetaWithDefaults) => {
assert(txMetaWithDefaults.txParams.value, '0x0', 'should have added 0x0 as the value')
assert(txMetaWithDefaults.txParams.gasPrice, 'should have added the gas price')
@ -210,99 +239,6 @@ describe('Transaction Controller', function () {
})
})
describe('#_validateTxParams', function () {
it('does not throw for positive values', function () {
var sample = {
from: '0x1678a085c290ebd122dc42cba69373b5953b831d',
value: '0x01',
}
txController._validateTxParams(sample)
})
it('returns error for negative values', function () {
var sample = {
from: '0x1678a085c290ebd122dc42cba69373b5953b831d',
value: '-0x01',
}
try {
txController._validateTxParams(sample)
} catch (err) {
assert.ok(err, 'error')
}
})
})
describe('#_normalizeTxParams', () => {
it('should normalize txParams', () => {
let txParams = {
chainId: '0x1',
from: 'a7df1beDBF813f57096dF77FCd515f0B3900e402',
to: null,
data: '68656c6c6f20776f726c64',
random: 'hello world',
}
let normalizedTxParams = txController._normalizeTxParams(txParams)
assert(!normalizedTxParams.chainId, 'their should be no chainId')
assert(!normalizedTxParams.to, 'their should be no to address if null')
assert.equal(normalizedTxParams.from.slice(0, 2), '0x', 'from should be hexPrefixd')
assert.equal(normalizedTxParams.data.slice(0, 2), '0x', 'data should be hexPrefixd')
assert(!('random' in normalizedTxParams), 'their should be no random key in normalizedTxParams')
txParams.to = 'a7df1beDBF813f57096dF77FCd515f0B3900e402'
normalizedTxParams = txController._normalizeTxParams(txParams)
assert.equal(normalizedTxParams.to.slice(0, 2), '0x', 'to should be hexPrefixd')
})
})
describe('#_validateRecipient', () => {
it('removes recipient for txParams with 0x when contract data is provided', function () {
const zeroRecipientandDataTxParams = {
from: '0x1678a085c290ebd122dc42cba69373b5953b831d',
to: '0x',
data: 'bytecode',
}
const sanitizedTxParams = txController._validateRecipient(zeroRecipientandDataTxParams)
assert.deepEqual(sanitizedTxParams, { from: '0x1678a085c290ebd122dc42cba69373b5953b831d', data: 'bytecode' }, 'no recipient with 0x')
})
it('should error when recipient is 0x', function () {
const zeroRecipientTxParams = {
from: '0x1678a085c290ebd122dc42cba69373b5953b831d',
to: '0x',
}
assert.throws(() => { txController._validateRecipient(zeroRecipientTxParams) }, Error, 'Invalid recipient address')
})
})
describe('#_validateFrom', () => {
it('should error when from is not a hex string', function () {
// where from is undefined
const txParams = {}
assert.throws(() => { txController._validateFrom(txParams) }, Error, `Invalid from address ${txParams.from} not a string`)
// where from is array
txParams.from = []
assert.throws(() => { txController._validateFrom(txParams) }, Error, `Invalid from address ${txParams.from} not a string`)
// where from is a object
txParams.from = {}
assert.throws(() => { txController._validateFrom(txParams) }, Error, `Invalid from address ${txParams.from} not a string`)
// where from is a invalid address
txParams.from = 'im going to fail'
assert.throws(() => { txController._validateFrom(txParams) }, Error, `Invalid from address`)
// should run
txParams.from ='0x1678a085c290ebd122dc42cba69373b5953b831d'
txController._validateFrom(txParams)
})
})
describe('#addTx', function () {
it('should emit updates', function (done) {
const txMeta = {
@ -391,12 +327,12 @@ describe('Transaction Controller', function () {
describe('#updateAndApproveTransaction', function () {
let txMeta
beforeEach(function () {
beforeEach(() => {
txMeta = {
id: 1,
status: 'unapproved',
txParams: {
from: '0xc684832530fcbddae4b4230a47e991ddcec2831d',
from: fromAccount.address,
to: '0x1678a085c290ebd122dc42cba69373b5953b831d',
gasPrice: '0x77359400',
gas: '0x7b0d',
@ -405,11 +341,12 @@ describe('Transaction Controller', function () {
metamaskNetworkId: currentNetworkId,
}
})
it('should update and approve transactions', function () {
it('should update and approve transactions', async () => {
txController.txStateManager.addTx(txMeta)
txController.updateAndApproveTransaction(txMeta)
const approvalPromise = txController.updateAndApproveTransaction(txMeta)
const tx = txController.txStateManager.getTx(1)
assert.equal(tx.status, 'approved')
await approvalPromise
})
})

View File

@ -1,14 +1,77 @@
const assert = require('assert')
const TxGasUtils = require('../../app/scripts/lib/tx-gas-utils')
const { createTestProviderTools } = require('../stub/provider')
const Transaction = require('ethereumjs-tx')
const BN = require('bn.js')
describe('Tx Gas Util', function () {
let txGasUtil, provider, providerResultStub
beforeEach(function () {
providerResultStub = {}
provider = createTestProviderTools({ scaffold: providerResultStub }).provider
txGasUtil = new TxGasUtils({
provider,
const { hexToBn, bnToHex } = require('../../app/scripts/lib/util')
const TxUtils = require('../../app/scripts/controllers/transactions/tx-gas-utils')
describe('txUtils', function () {
let txUtils
before(function () {
txUtils = new TxUtils(new Proxy({}, {
get: (obj, name) => {
return () => {}
},
}))
})
describe('chain Id', function () {
it('prepares a transaction with the provided chainId', function () {
const txParams = {
to: '0x70ad465e0bab6504002ad58c744ed89c7da38524',
from: '0x69ad465e0bab6504002ad58c744ed89c7da38525',
value: '0x0',
gas: '0x7b0c',
gasPrice: '0x199c82cc00',
data: '0x',
nonce: '0x3',
chainId: 42,
}
const ethTx = new Transaction(txParams)
assert.equal(ethTx.getChainId(), 42, 'chainId is set from tx params')
})
})
describe('addGasBuffer', function () {
it('multiplies by 1.5, when within block gas limit', function () {
// naive estimatedGas: 0x16e360 (1.5 mil)
const inputHex = '0x16e360'
// dummy gas limit: 0x3d4c52 (4 mil)
const blockGasLimitHex = '0x3d4c52'
const output = txUtils.addGasBuffer(inputHex, blockGasLimitHex)
const inputBn = hexToBn(inputHex)
const outputBn = hexToBn(output)
const expectedBn = inputBn.muln(1.5)
assert(outputBn.eq(expectedBn), 'returns 1.5 the input value')
})
it('uses original estimatedGas, when above block gas limit', function () {
// naive estimatedGas: 0x16e360 (1.5 mil)
const inputHex = '0x16e360'
// dummy gas limit: 0x0f4240 (1 mil)
const blockGasLimitHex = '0x0f4240'
const output = txUtils.addGasBuffer(inputHex, blockGasLimitHex)
// const inputBn = hexToBn(inputHex)
const outputBn = hexToBn(output)
const expectedBn = hexToBn(inputHex)
assert(outputBn.eq(expectedBn), 'returns the original estimatedGas value')
})
it('buffers up to recommend gas limit recommended ceiling', function () {
// naive estimatedGas: 0x16e360 (1.5 mil)
const inputHex = '0x16e360'
// dummy gas limit: 0x1e8480 (2 mil)
const blockGasLimitHex = '0x1e8480'
const blockGasLimitBn = hexToBn(blockGasLimitHex)
const ceilGasLimitBn = blockGasLimitBn.muln(0.9)
const output = txUtils.addGasBuffer(inputHex, blockGasLimitHex)
// const inputBn = hexToBn(inputHex)
// const outputBn = hexToBn(output)
const expectedHex = bnToHex(ceilGasLimitBn)
assert.equal(output, expectedHex, 'returns the gas limit recommended ceiling value')
})
})
})

View File

@ -1,6 +1,6 @@
const assert = require('assert')
const clone = require('clone')
const txStateHistoryHelper = require('../../app/scripts/lib/tx-state-history-helper')
const txStateHistoryHelper = require('../../app/scripts/controllers/transactions/lib/tx-state-history-helper')
describe('deepCloneFromTxMeta', function () {
it('should clone deep', function () {

View File

@ -1,5 +1,5 @@
const assert = require('assert')
const txStateHistoryHelper = require('../../app/scripts/lib/tx-state-history-helper')
const txStateHistoryHelper = require('../../app/scripts/controllers/transactions/lib/tx-state-history-helper')
const testVault = require('../data/v17-long-history.json')

View File

@ -1,8 +1,8 @@
const assert = require('assert')
const clone = require('clone')
const ObservableStore = require('obs-store')
const TxStateManager = require('../../app/scripts/lib/tx-state-manager')
const txStateHistoryHelper = require('../../app/scripts/lib/tx-state-history-helper')
const TxStateManager = require('../../app/scripts/controllers/transactions/tx-state-manager')
const txStateHistoryHelper = require('../../app/scripts/controllers/transactions/lib/tx-state-history-helper')
const noop = () => true
describe('TransactionStateManager', function () {

View File

@ -1,77 +1,98 @@
const assert = require('assert')
const Transaction = require('ethereumjs-tx')
const BN = require('bn.js')
const { hexToBn, bnToHex } = require('../../app/scripts/lib/util')
const TxUtils = require('../../app/scripts/lib/tx-gas-utils')
const txUtils = require('../../app/scripts/controllers/transactions/lib/util')
describe('txUtils', function () {
let txUtils
before(function () {
txUtils = new TxUtils(new Proxy({}, {
get: (obj, name) => {
return () => {}
},
}))
})
describe('chain Id', function () {
it('prepares a transaction with the provided chainId', function () {
const txParams = {
to: '0x70ad465e0bab6504002ad58c744ed89c7da38524',
from: '0x69ad465e0bab6504002ad58c744ed89c7da38525',
value: '0x0',
gas: '0x7b0c',
gasPrice: '0x199c82cc00',
data: '0x',
nonce: '0x3',
chainId: 42,
describe('#validateTxParams', function () {
it('does not throw for positive values', function () {
var sample = {
from: '0x1678a085c290ebd122dc42cba69373b5953b831d',
value: '0x01',
}
txUtils.validateTxParams(sample)
})
it('returns error for negative values', function () {
var sample = {
from: '0x1678a085c290ebd122dc42cba69373b5953b831d',
value: '-0x01',
}
try {
txUtils.validateTxParams(sample)
} catch (err) {
assert.ok(err, 'error')
}
const ethTx = new Transaction(txParams)
assert.equal(ethTx.getChainId(), 42, 'chainId is set from tx params')
})
})
describe('addGasBuffer', function () {
it('multiplies by 1.5, when within block gas limit', function () {
// naive estimatedGas: 0x16e360 (1.5 mil)
const inputHex = '0x16e360'
// dummy gas limit: 0x3d4c52 (4 mil)
const blockGasLimitHex = '0x3d4c52'
const output = txUtils.addGasBuffer(inputHex, blockGasLimitHex)
const inputBn = hexToBn(inputHex)
const outputBn = hexToBn(output)
const expectedBn = inputBn.muln(1.5)
assert(outputBn.eq(expectedBn), 'returns 1.5 the input value')
})
describe('#normalizeTxParams', () => {
it('should normalize txParams', () => {
let txParams = {
chainId: '0x1',
from: 'a7df1beDBF813f57096dF77FCd515f0B3900e402',
to: null,
data: '68656c6c6f20776f726c64',
random: 'hello world',
}
it('uses original estimatedGas, when above block gas limit', function () {
// naive estimatedGas: 0x16e360 (1.5 mil)
const inputHex = '0x16e360'
// dummy gas limit: 0x0f4240 (1 mil)
const blockGasLimitHex = '0x0f4240'
const output = txUtils.addGasBuffer(inputHex, blockGasLimitHex)
// const inputBn = hexToBn(inputHex)
const outputBn = hexToBn(output)
const expectedBn = hexToBn(inputHex)
assert(outputBn.eq(expectedBn), 'returns the original estimatedGas value')
})
let normalizedTxParams = txUtils.normalizeTxParams(txParams)
assert(!normalizedTxParams.chainId, 'their should be no chainId')
assert(!normalizedTxParams.to, 'their should be no to address if null')
assert.equal(normalizedTxParams.from.slice(0, 2), '0x', 'from should be hexPrefixd')
assert.equal(normalizedTxParams.data.slice(0, 2), '0x', 'data should be hexPrefixd')
assert(!('random' in normalizedTxParams), 'their should be no random key in normalizedTxParams')
txParams.to = 'a7df1beDBF813f57096dF77FCd515f0B3900e402'
normalizedTxParams = txUtils.normalizeTxParams(txParams)
assert.equal(normalizedTxParams.to.slice(0, 2), '0x', 'to should be hexPrefixd')
it('buffers up to recommend gas limit recommended ceiling', function () {
// naive estimatedGas: 0x16e360 (1.5 mil)
const inputHex = '0x16e360'
// dummy gas limit: 0x1e8480 (2 mil)
const blockGasLimitHex = '0x1e8480'
const blockGasLimitBn = hexToBn(blockGasLimitHex)
const ceilGasLimitBn = blockGasLimitBn.muln(0.9)
const output = txUtils.addGasBuffer(inputHex, blockGasLimitHex)
// const inputBn = hexToBn(inputHex)
// const outputBn = hexToBn(output)
const expectedHex = bnToHex(ceilGasLimitBn)
assert.equal(output, expectedHex, 'returns the gas limit recommended ceiling value')
})
})
})
describe('#validateRecipient', () => {
it('removes recipient for txParams with 0x when contract data is provided', function () {
const zeroRecipientandDataTxParams = {
from: '0x1678a085c290ebd122dc42cba69373b5953b831d',
to: '0x',
data: 'bytecode',
}
const sanitizedTxParams = txUtils.validateRecipient(zeroRecipientandDataTxParams)
assert.deepEqual(sanitizedTxParams, { from: '0x1678a085c290ebd122dc42cba69373b5953b831d', data: 'bytecode' }, 'no recipient with 0x')
})
it('should error when recipient is 0x', function () {
const zeroRecipientTxParams = {
from: '0x1678a085c290ebd122dc42cba69373b5953b831d',
to: '0x',
}
assert.throws(() => { txUtils.validateRecipient(zeroRecipientTxParams) }, Error, 'Invalid recipient address')
})
})
describe('#validateFrom', () => {
it('should error when from is not a hex string', function () {
// where from is undefined
const txParams = {}
assert.throws(() => { txUtils.validateFrom(txParams) }, Error, `Invalid from address ${txParams.from} not a string`)
// where from is array
txParams.from = []
assert.throws(() => { txUtils.validateFrom(txParams) }, Error, `Invalid from address ${txParams.from} not a string`)
// where from is a object
txParams.from = {}
assert.throws(() => { txUtils.validateFrom(txParams) }, Error, `Invalid from address ${txParams.from} not a string`)
// where from is a invalid address
txParams.from = 'im going to fail'
assert.throws(() => { txUtils.validateFrom(txParams) }, Error, `Invalid from address`)
// should run
txParams.from ='0x1678a085c290ebd122dc42cba69373b5953b831d'
txUtils.validateFrom(txParams)
})
})
})

View File

@ -1,4 +1,5 @@
const abi = require('human-standard-token-abi')
const pify = require('pify')
const getBuyEthUrl = require('../../app/scripts/lib/buy-eth-url')
const { getTokenAddressFromTokenObject } = require('./util')
const ethUtil = require('ethereumjs-util')
@ -83,7 +84,7 @@ var actions = {
REVEAL_SEED_CONFIRMATION: 'REVEAL_SEED_CONFIRMATION',
revealSeedConfirmation: revealSeedConfirmation,
requestRevealSeed: requestRevealSeed,
requestRevealSeedWords,
// unlock screen
UNLOCK_IN_PROGRESS: 'UNLOCK_IN_PROGRESS',
UNLOCK_FAILED: 'UNLOCK_FAILED',
@ -345,11 +346,13 @@ function transitionBackward () {
}
}
function clearSeedWordCache () {
log.debug(`background.clearSeedWordCache`)
function confirmSeedWords () {
return dispatch => {
dispatch(actions.showLoadingIndication())
log.debug(`background.clearSeedWordCache`)
return new Promise((resolve, reject) => {
background.clearSeedWordCache((err, account) => {
dispatch(actions.hideLoadingIndication())
if (err) {
dispatch(actions.displayWarning(err.message))
return reject(err)
@ -363,22 +366,6 @@ function clearSeedWordCache () {
}
}
function confirmSeedWords () {
return async dispatch => {
dispatch(actions.showLoadingIndication())
const account = await dispatch(clearSeedWordCache())
return dispatch(setIsRevealingSeedWords(false))
.then(() => {
dispatch(actions.hideLoadingIndication())
return account
})
.catch(() => {
dispatch(actions.hideLoadingIndication())
return account
})
}
}
function createNewVaultAndRestore (password, seed) {
return (dispatch) => {
dispatch(actions.showLoadingIndication())
@ -441,6 +428,30 @@ function revealSeedConfirmation () {
}
}
function verifyPassword (password) {
return new Promise((resolve, reject) => {
background.submitPassword(password, error => {
if (error) {
return reject(error)
}
resolve(true)
})
})
}
function verifySeedPhrase () {
return new Promise((resolve, reject) => {
background.verifySeedPhrase((error, seedWords) => {
if (error) {
return reject(error)
}
resolve(seedWords)
})
})
}
function requestRevealSeed (password) {
return dispatch => {
dispatch(actions.showLoadingIndication())
@ -460,13 +471,29 @@ function requestRevealSeed (password) {
}
dispatch(actions.showNewVaultSeed(result))
dispatch(actions.hideLoadingIndication())
resolve()
})
})
})
.then(() => dispatch(setIsRevealingSeedWords(true)))
.then(() => dispatch(actions.hideLoadingIndication()))
.catch(() => dispatch(actions.hideLoadingIndication()))
}
}
function requestRevealSeedWords (password) {
return async dispatch => {
dispatch(actions.showLoadingIndication())
log.debug(`background.submitPassword`)
try {
await verifyPassword(password)
const seedWords = await verifySeedPhrase()
dispatch(actions.hideLoadingIndication())
return seedWords
} catch (error) {
dispatch(actions.hideLoadingIndication())
dispatch(actions.displayWarning(error.message))
throw new Error(error.message)
}
}
}
@ -497,31 +524,26 @@ function addNewKeyring (type, opts) {
}
function importNewAccount (strategy, args) {
return (dispatch) => {
dispatch(actions.showLoadingIndication('This may take a while, be patient.'))
log.debug(`background.importAccountWithStrategy`)
return new Promise((resolve, reject) => {
background.importAccountWithStrategy(strategy, args, (err) => {
if (err) {
dispatch(actions.displayWarning(err.message))
return reject(err)
}
log.debug(`background.getState`)
background.getState((err, newState) => {
dispatch(actions.hideLoadingIndication())
if (err) {
dispatch(actions.displayWarning(err.message))
return reject(err)
}
dispatch(actions.updateMetamaskState(newState))
dispatch({
type: actions.SHOW_ACCOUNT_DETAIL,
value: newState.selectedAddress,
})
resolve(newState)
})
})
return async (dispatch) => {
let newState
dispatch(actions.showLoadingIndication('This may take a while, please be patient.'))
try {
log.debug(`background.importAccountWithStrategy`)
await pify(background.importAccountWithStrategy).call(background, strategy, args)
log.debug(`background.getState`)
newState = await pify(background.getState).call(background)
} catch (err) {
dispatch(actions.hideLoadingIndication())
dispatch(actions.displayWarning(err.message))
throw err
}
dispatch(actions.hideLoadingIndication())
dispatch(actions.updateMetamaskState(newState))
dispatch({
type: actions.SHOW_ACCOUNT_DETAIL,
value: newState.selectedAddress,
})
return newState
}
}
@ -1923,11 +1945,3 @@ function updateNetworkEndpointType (networkEndpointType) {
value: networkEndpointType,
}
}
function setIsRevealingSeedWords (reveal) {
return dispatch => {
log.debug(`background.setIsRevealingSeedWords`)
background.setIsRevealingSeedWords(reveal)
return forceUpdateMetamaskState(dispatch)
}
}

View File

@ -24,12 +24,12 @@ const Initialized = require('./components/pages/initialized')
const Settings = require('./components/pages/settings')
const UnlockPage = require('./components/pages/unlock')
const RestoreVaultPage = require('./components/pages/keychains/restore-vault')
const RevealSeedConfirmation = require('./keychains/hd/recover-seed/confirmation')
const RevealSeedConfirmation = require('./components/pages/keychains/reveal-seed')
const AddTokenPage = require('./components/pages/add-token')
const CreateAccountPage = require('./components/pages/create-account')
const NoticeScreen = require('./components/pages/notice')
const Loading = require('./components/loading')
const Loading = require('./components/loading-screen')
const NetworkIndicator = require('./components/network')
const Identicon = require('./components/identicon')
const ReactCSSTransitionGroup = require('react-addons-css-transition-group')
@ -56,20 +56,11 @@ const {
class App extends Component {
componentWillMount () {
const {
currentCurrency,
setCurrentCurrencyToUSD,
isRevealingSeedWords,
clearSeedWords,
} = this.props
const { currentCurrency, setCurrentCurrencyToUSD } = this.props
if (!currentCurrency) {
setCurrentCurrencyToUSD()
}
if (isRevealingSeedWords) {
clearSeedWords()
}
}
renderRoutes () {
@ -144,6 +135,7 @@ class App extends Component {
(isLoading || isLoadingNetwork) && h(Loading, {
loadingMessage: loadMessage,
fullScreen: true,
}),
// content
@ -402,8 +394,6 @@ App.propTypes = {
isMouseUser: PropTypes.bool,
setMouseUserState: PropTypes.func,
t: PropTypes.func,
isRevealingSeedWords: PropTypes.bool,
clearSeedWords: PropTypes.func,
}
function mapStateToProps (state) {
@ -484,7 +474,6 @@ function mapDispatchToProps (dispatch, ownProps) {
setCurrentCurrencyToUSD: () => dispatch(actions.setCurrentCurrency('usd')),
toggleAccountMenu: () => dispatch(actions.toggleAccountMenu()),
setMouseUserState: (isMouseUser) => dispatch(actions.setMouseUserState(isMouseUser)),
clearSeedWords: () => dispatch(actions.confirmSeedWords()),
}
}

View File

@ -6,10 +6,10 @@ const connect = require('react-redux').connect
const actions = require('../actions')
const CoinbaseForm = require('./coinbase-form')
const ShapeshiftForm = require('./shapeshift-form')
const Loading = require('./loading')
const Loading = require('./loading-screen')
const AccountPanel = require('./account-panel')
const RadioList = require('./custom-radio-list')
const networkNames = require('../../../app/scripts/config.js').networkNames
const { getNetworkDisplayName } = require('../../../app/scripts/controllers/network/util')
BuyButtonSubview.contextTypes = {
t: PropTypes.func,
@ -148,7 +148,7 @@ BuyButtonSubview.prototype.primarySubview = function () {
case '3':
case '4':
case '42':
const networkName = networkNames[network]
const networkName = getNetworkDisplayName(network)
const label = `${networkName} ${this.context.t('testFaucet')}`
return (
h('div.flex-column', {

View File

@ -280,8 +280,7 @@ CustomizeGasModal.prototype.render = function () {
h(GasModalCard, {
value: convertedGasPrice,
min: forceGasMin || MIN_GAS_PRICE_GWEI,
// max: 1000,
step: multiplyCurrencies(MIN_GAS_PRICE_GWEI, 10),
step: 1,
onChange: value => this.convertAndSetGasPrice(value),
title: this.context.t('gasPrice'),
copy: this.context.t('gasPriceCalculation'),
@ -290,7 +289,6 @@ CustomizeGasModal.prototype.render = function () {
h(GasModalCard, {
value: convertedGasLimit,
min: 1,
// max: 100000,
step: 1,
onChange: value => this.convertAndSetGasLimit(value),
title: this.context.t('gasLimit'),

View File

@ -3,12 +3,14 @@ const PropTypes = require('prop-types')
const h = require('react-hyperscript')
const inherits = require('util').inherits
const connect = require('react-redux').connect
const { withRouter } = require('react-router-dom')
const { compose } = require('recompose')
const actions = require('../../actions')
const Dropdown = require('./components/dropdown').Dropdown
const DropdownMenuItem = require('./components/dropdown').DropdownMenuItem
const NetworkDropdownIcon = require('./components/network-dropdown-icon')
const R = require('ramda')
const { SETTINGS_ROUTE } = require('../../routes')
// classes from nodes of the toggle element.
const notToggleElementClassnames = [
@ -41,9 +43,6 @@ function mapDispatchToProps (dispatch) {
setRpcTarget: (target) => {
dispatch(actions.setRpcTarget(target))
},
showConfigPage: () => {
dispatch(actions.showConfigPage())
},
showNetworkDropdown: () => dispatch(actions.showNetworkDropdown()),
hideNetworkDropdown: () => dispatch(actions.hideNetworkDropdown()),
}
@ -59,7 +58,10 @@ NetworkDropdown.contextTypes = {
t: PropTypes.func,
}
module.exports = connect(mapStateToProps, mapDispatchToProps)(NetworkDropdown)
module.exports = compose(
withRouter,
connect(mapStateToProps, mapDispatchToProps)
)(NetworkDropdown)
// TODO: specify default props and proptypes
@ -227,7 +229,7 @@ NetworkDropdown.prototype.render = function () {
DropdownMenuItem,
{
closeMenu: () => this.props.hideNetworkDropdown(),
onClick: () => this.props.showConfigPage(),
onClick: () => this.props.history.push(SETTINGS_ROUTE),
style: dropdownMenuItemStyle,
},
[

View File

@ -0,0 +1,45 @@
const { Component } = require('react')
const PropTypes = require('prop-types')
const h = require('react-hyperscript')
const copyToClipboard = require('copy-to-clipboard')
const { exportAsFile } = require('../../util')
class ExportTextContainer extends Component {
render () {
const { text = '', filename = '' } = this.props
const { t } = this.context
return (
h('.export-text-container', [
h('.export-text-container__text-container', [
h('.export-text-container__text', text),
]),
h('.export-text-container__buttons-container', [
h('.export-text-container__button.export-text-container__button--copy', {
onClick: () => copyToClipboard(text),
}, [
h('img', { src: 'images/copy-to-clipboard.svg' }),
h('.export-text-container__button-text', t('copyToClipboard')),
]),
h('.export-text-container__button', {
onClick: () => exportAsFile(filename, text),
}, [
h('img', { src: 'images/download.svg' }),
h('.export-text-container__button-text', t('saveAsCsvFile')),
]),
]),
])
)
}
}
ExportTextContainer.propTypes = {
text: PropTypes.string,
filename: PropTypes.string,
}
ExportTextContainer.contextTypes = {
t: PropTypes.func,
}
module.exports = ExportTextContainer

View File

@ -0,0 +1,52 @@
.export-text-container {
display: flex;
justify-content: center;
flex-direction: column;
align-items: center;
border: 1px solid $alto;
border-radius: 4px;
font-weight: 400;
&__text-container {
width: 100%;
display: flex;
justify-content: center;
padding: 20px;
border-radius: 4px;
background: $alabaster;
}
&__text {
resize: none;
border: none;
background: $alabaster;
font-size: 20px;
text-align: center;
}
&__buttons-container {
display: flex;
flex-direction: row;
border-top: 1px solid $alto;
width: 100%;
}
&__button {
padding: 10px;
flex: 1;
display: flex;
justify-content: center;
align-items: center;
font-size: 14px;
cursor: pointer;
color: $curious-blue;
&--copy {
border-right: 1px solid $alto;
}
}
&__button-text {
padding-left: 10px;
}
}

View File

@ -0,0 +1,2 @@
const ExportTextContainer = require('./export-text-container.component')
module.exports = ExportTextContainer

View File

@ -0,0 +1,2 @@
const LoadingScreen = require('./loading-screen.component')
module.exports = LoadingScreen

View File

@ -2,8 +2,9 @@ const { Component } = require('react')
const h = require('react-hyperscript')
const PropTypes = require('prop-types')
const classnames = require('classnames')
const Spinner = require('../spinner')
class LoadingIndicator extends Component {
class LoadingScreen extends Component {
renderMessage () {
const { loadingMessage } = this.props
return loadingMessage && h('span', loadingMessage)
@ -14,9 +15,9 @@ class LoadingIndicator extends Component {
h('.loading-overlay', {
className: classnames({ 'loading-overlay--full-screen': this.props.fullScreen }),
}, [
h('.flex-center.flex-column', [
h('img', {
src: 'images/loading.svg',
h('.loading-overlay__container', [
h(Spinner, {
color: '#F7C06C',
}),
this.renderMessage(),
@ -26,9 +27,9 @@ class LoadingIndicator extends Component {
}
}
LoadingIndicator.propTypes = {
LoadingScreen.propTypes = {
loadingMessage: PropTypes.string,
fullScreen: PropTypes.bool,
}
module.exports = LoadingIndicator
module.exports = LoadingScreen

View File

@ -4,7 +4,7 @@ const h = require('react-hyperscript')
const inherits = require('util').inherits
const connect = require('react-redux').connect
const actions = require('../../actions')
const networkNames = require('../../../../app/scripts/config.js').networkNames
const { getNetworkDisplayName } = require('../../../../app/scripts/controllers/network/util')
function mapStateToProps (state) {
return {
@ -52,7 +52,7 @@ BuyOptions.prototype.renderModalContentOption = function (title, header, onClick
BuyOptions.prototype.render = function () {
const { network, toCoinbase, address, toFaucet } = this.props
const isTestNetwork = ['3', '4', '42'].find(n => n === network)
const networkName = networkNames[network]
const networkName = getNetworkDisplayName(network)
return h('div', {}, [
h('div.buy-modal-content.transfers-subview', {

View File

@ -4,7 +4,7 @@ const h = require('react-hyperscript')
const inherits = require('util').inherits
const connect = require('react-redux').connect
const actions = require('../../actions')
const networkNames = require('../../../../app/scripts/config.js').networkNames
const { getNetworkDisplayName } = require('../../../../app/scripts/controllers/network/util')
const ShapeshiftForm = require('../shapeshift-form')
let DIRECT_DEPOSIT_ROW_TITLE
@ -122,7 +122,7 @@ DepositEtherModal.prototype.render = function () {
const { buyingWithShapeshift } = this.state
const isTestNetwork = ['3', '4', '42'].find(n => n === network)
const networkName = networkNames[network]
const networkName = getNetworkDisplayName(network)
return h('div.page-container.page-container--full-width.page-container--full-height', {}, [

View File

@ -192,7 +192,7 @@ AddTokenScreen.prototype.attemptToAutoFillTokenParams = async function (address)
if (symbol && decimals) {
this.setState({
customSymbol: symbol,
customDecimals: decimals.toString(),
customDecimals: decimals,
autoFilled: true,
})
}

View File

@ -105,6 +105,8 @@ class JsonImportSubview extends Component {
}
this.props.importNewJsonAccount([ fileContents, password ])
// JS runtime requires caught rejections but failures are handled by Redux
.catch()
}
}

View File

@ -91,5 +91,7 @@ PrivateKeyImportView.prototype.createNewKeychain = function () {
const { importNewAccount, history } = this.props
importNewAccount('Private Key', [ privateKey ])
// JS runtime requires caught rejections but failures are handled by Redux
.catch()
.then(() => history.push(DEFAULT_ROUTE))
}

View File

@ -21,7 +21,7 @@ const QrView = require('../../components/qr-code')
// Routes
const {
REVEAL_SEED_ROUTE,
INITIALIZE_BACKUP_PHRASE_ROUTE,
RESTORE_VAULT_ROUTE,
CONFIRM_TRANSACTION_ROUTE,
NOTICE_ROUTE,
@ -69,7 +69,7 @@ class Home extends Component {
log.debug('rendering seed words')
return h(Redirect, {
to: {
pathname: REVEAL_SEED_ROUTE,
pathname: INITIALIZE_BACKUP_PHRASE_ROUTE,
},
})
}

View File

@ -2,11 +2,27 @@ const { Component } = require('react')
const { connect } = require('react-redux')
const PropTypes = require('prop-types')
const h = require('react-hyperscript')
const { exportAsFile } = require('../../../util')
const { requestRevealSeed, confirmSeedWords } = require('../../../actions')
const classnames = require('classnames')
const { requestRevealSeedWords } = require('../../../actions')
const { DEFAULT_ROUTE } = require('../../../routes')
const ExportTextContainer = require('../../export-text-container')
const PASSWORD_PROMPT_SCREEN = 'PASSWORD_PROMPT_SCREEN'
const REVEAL_SEED_SCREEN = 'REVEAL_SEED_SCREEN'
class RevealSeedPage extends Component {
constructor (props) {
super(props)
this.state = {
screen: PASSWORD_PROMPT_SCREEN,
password: '',
seedWords: null,
error: null,
}
}
componentDidMount () {
const passwordBox = document.getElementById('password-box')
if (passwordBox) {
@ -14,182 +30,135 @@ class RevealSeedPage extends Component {
}
}
checkConfirmation (event) {
if (event.key === 'Enter') {
event.preventDefault()
this.revealSeedWords()
}
handleSubmit (event) {
event.preventDefault()
this.setState({ seedWords: null, error: null })
this.props.requestRevealSeedWords(this.state.password)
.then(seedWords => this.setState({ seedWords, screen: REVEAL_SEED_SCREEN }))
.catch(error => this.setState({ error: error.message }))
}
revealSeedWords () {
const password = document.getElementById('password-box').value
this.props.requestRevealSeed(password)
}
renderSeed () {
const { seedWords, confirmSeedWords, history } = this.props
renderWarning () {
return (
h('.initialize-screen.flex-column.flex-center.flex-grow', [
h('h3.flex-center.text-transform-uppercase', {
style: {
background: '#EBEBEB',
color: '#AEAEAE',
marginTop: 36,
marginBottom: 8,
width: '100%',
fontSize: '20px',
padding: 6,
},
}, [
'Vault Created',
]),
h('div', {
style: {
fontSize: '1em',
marginTop: '10px',
textAlign: 'center',
},
}, [
h('span.error', 'These 12 words are the only way to restore your MetaMask accounts.\nSave them somewhere safe and secret.'),
]),
h('textarea.twelve-word-phrase', {
readOnly: true,
value: seedWords,
h('.page-container__warning-container', [
h('img.page-container__warning-icon', {
src: 'images/warning.svg',
}),
h('button.primary', {
onClick: () => confirmSeedWords().then(() => history.push(DEFAULT_ROUTE)),
style: {
margin: '24px',
fontSize: '0.9em',
marginBottom: '10px',
},
}, 'I\'ve copied it somewhere safe'),
h('button.primary', {
onClick: () => exportAsFile(`MetaMask Seed Words`, seedWords),
style: {
margin: '10px',
fontSize: '0.9em',
},
}, 'Save Seed Words As File'),
h('.page-container__warning-message', [
h('.page-container__warning-title', [this.context.t('revealSeedWordsWarningTitle')]),
h('div', [this.context.t('revealSeedWordsWarning')]),
]),
])
)
}
renderConfirmation () {
const { history, warning, inProgress } = this.props
renderContent () {
return this.state.screen === PASSWORD_PROMPT_SCREEN
? this.renderPasswordPromptContent()
: this.renderRevealSeedContent()
}
renderPasswordPromptContent () {
const { t } = this.context
return (
h('.initialize-screen.flex-column.flex-center.flex-grow', {
style: { maxWidth: '420px' },
h('form', {
onSubmit: event => this.handleSubmit(event),
}, [
h('h3.flex-center.text-transform-uppercase', {
style: {
background: '#EBEBEB',
color: '#AEAEAE',
marginBottom: 24,
width: '100%',
fontSize: '20px',
padding: 6,
},
}, [
'Reveal Seed Words',
]),
h('.div', {
style: {
display: 'flex',
flexDirection: 'column',
padding: '20px',
justifyContent: 'center',
},
}, [
h('h4', 'Do not recover your seed words in a public place! These words can be used to steal all your accounts.'),
// confirmation
h('input.large-input.letter-spacey', {
h('label.input-label', {
htmlFor: 'password-box',
}, t('enterPasswordContinue')),
h('.input-group', [
h('input.form-control', {
type: 'password',
placeholder: t('password'),
id: 'password-box',
placeholder: 'Enter your password to confirm',
onKeyPress: this.checkConfirmation.bind(this),
style: {
width: 260,
marginTop: '12px',
},
value: this.state.password,
onChange: event => this.setState({ password: event.target.value }),
className: classnames({ 'form-control--error': this.state.error }),
}),
h('.flex-row.flex-start', {
style: {
marginTop: 30,
width: '50%',
},
}, [
// cancel
h('button.primary', {
onClick: () => history.push(DEFAULT_ROUTE),
}, 'CANCEL'),
// submit
h('button.primary', {
style: { marginLeft: '10px' },
onClick: this.revealSeedWords.bind(this),
}, 'OK'),
]),
warning && (
h('span.error', {
style: {
margin: '20px',
},
}, warning.split('-'))
),
inProgress && (
h('span.in-progress-notification', 'Generating Seed...')
),
]),
this.state.error && h('.reveal-seed__error', this.state.error),
])
)
}
renderRevealSeedContent () {
const { t } = this.context
return (
h('div', [
h('label.reveal-seed__label', t('yourPrivateSeedPhrase')),
h(ExportTextContainer, {
text: this.state.seedWords,
filename: t('metamaskSeedWords'),
}),
])
)
}
renderFooter () {
return this.state.screen === PASSWORD_PROMPT_SCREEN
? this.renderPasswordPromptFooter()
: this.renderRevealSeedFooter()
}
renderPasswordPromptFooter () {
return (
h('.page-container__footer', [
h('button.btn-secondary--lg.page-container__footer-button', {
onClick: () => this.props.history.push(DEFAULT_ROUTE),
}, this.context.t('cancel')),
h('button.btn-primary--lg.page-container__footer-button', {
onClick: event => this.handleSubmit(event),
disabled: this.state.password === '',
}, this.context.t('next')),
])
)
}
renderRevealSeedFooter () {
return (
h('.page-container__footer', [
h('button.btn-secondary--lg.page-container__footer-button', {
onClick: () => this.props.history.push(DEFAULT_ROUTE),
}, this.context.t('close')),
])
)
}
render () {
return this.props.seedWords
? this.renderSeed()
: this.renderConfirmation()
return (
h('.page-container', [
h('.page-container__header', [
h('.page-container__title', this.context.t('revealSeedWordsTitle')),
h('.page-container__subtitle', this.context.t('revealSeedWordsDescription')),
]),
h('.page-container__content', [
this.renderWarning(),
h('.reveal-seed__content', [
this.renderContent(),
]),
]),
this.renderFooter(),
])
)
}
}
RevealSeedPage.propTypes = {
requestRevealSeed: PropTypes.func,
confirmSeedWords: PropTypes.func,
seedWords: PropTypes.string,
inProgress: PropTypes.bool,
requestRevealSeedWords: PropTypes.func,
history: PropTypes.object,
warning: PropTypes.string,
}
const mapStateToProps = state => {
const { appState: { warning }, metamask: { seedWords } } = state
return {
warning,
seedWords,
}
RevealSeedPage.contextTypes = {
t: PropTypes.func,
}
const mapDispatchToProps = dispatch => {
return {
requestRevealSeed: password => dispatch(requestRevealSeed(password)),
confirmSeedWords: () => dispatch(confirmSeedWords()),
requestRevealSeedWords: password => dispatch(requestRevealSeedWords(password)),
}
}
module.exports = connect(mapStateToProps, mapDispatchToProps)(RevealSeedPage)
module.exports = connect(null, mapDispatchToProps)(RevealSeedPage)

View File

@ -12,7 +12,7 @@ const SimpleDropdown = require('../../dropdowns/simple-dropdown')
const ToggleButton = require('react-toggle-button')
const { REVEAL_SEED_ROUTE } = require('../../../routes')
const locales = require('../../../../../app/_locales/index.json')
const { OLD_UI_NETWORK_TYPE } = require('../../../../../app/scripts/config').enums
const { OLD_UI_NETWORK_TYPE } = require('../../../../../app/scripts/controllers/network/enums')
const getInfuraCurrencyOptions = () => {
const sortedCurrencies = infuraCurrencies.objects.sort((a, b) => {

View File

@ -16,7 +16,7 @@ const { getEnvironmentType } = require('../../../../app/scripts/lib/util')
const getCaretCoordinates = require('textarea-caret')
const EventEmitter = require('events').EventEmitter
const Mascot = require('../mascot')
const { OLD_UI_NETWORK_TYPE } = require('../../../../app/scripts/config').enums
const { OLD_UI_NETWORK_TYPE } = require('../../../../app/scripts/controllers/network/enums')
const { DEFAULT_ROUTE, RESTORE_VAULT_ROUTE } = require('../../routes')
class UnlockScreen extends Component {

View File

@ -8,11 +8,11 @@ const abiDecoder = require('abi-decoder')
abiDecoder.addABI(abi)
const inherits = require('util').inherits
const actions = require('../../actions')
const util = require('../../util')
const { getSymbolAndDecimals } = require('../../token-util')
const ConfirmSendEther = require('./confirm-send-ether')
const ConfirmSendToken = require('./confirm-send-token')
const ConfirmDeployContract = require('./confirm-deploy-contract')
const Loading = require('../loading')
const Loading = require('../loading-screen')
const TX_TYPES = {
DEPLOY_CONTRACT: 'deploy_contract',
@ -26,6 +26,7 @@ function mapStateToProps (state) {
const {
conversionRate,
identities,
tokens: existingTokens,
} = state.metamask
const accounts = state.metamask.accounts
const selectedAddress = state.metamask.selectedAddress || Object.keys(accounts)[0]
@ -33,6 +34,7 @@ function mapStateToProps (state) {
conversionRate,
identities,
selectedAddress,
existingTokens,
}
}
@ -66,6 +68,7 @@ PendingTx.prototype.componentDidUpdate = function (prevProps, prevState) {
}
PendingTx.prototype.setTokenData = async function () {
const { existingTokens } = this.props
const txMeta = this.gatherTxMeta()
const txParams = txMeta.txParams || {}
@ -89,30 +92,15 @@ PendingTx.prototype.setTokenData = async function () {
}
if (isTokenTransaction) {
const token = util.getContractAtAddress(txParams.to)
const results = await Promise.all([
token.symbol(),
token.decimals(),
])
const [ symbol, decimals ] = results
const { symbol, decimals } = await getSymbolAndDecimals(txParams.to, existingTokens)
if (symbol[0] && decimals[0]) {
this.setState({
transactionType: TX_TYPES.SEND_TOKEN,
tokenAddress: txParams.to,
tokenSymbol: symbol[0],
tokenDecimals: decimals[0],
isFetching: false,
})
} else {
this.setState({
transactionType: TX_TYPES.SEND_TOKEN,
tokenAddress: txParams.to,
tokenSymbol: null,
tokenDecimals: null,
isFetching: false,
})
}
this.setState({
transactionType: TX_TYPES.SEND_TOKEN,
tokenAddress: txParams.to,
tokenSymbol: symbol,
tokenDecimals: decimals,
isFetching: false,
})
} else {
this.setState({
transactionType: TX_TYPES.SEND_ETHER,

View File

@ -89,7 +89,6 @@ CurrencyDisplay.prototype.render = function () {
} = this.props
const valueToRender = this.getValueToRender()
const convertedValueToRender = this.getConvertedValueToRender(valueToRender)
return h('div', {
@ -97,22 +96,24 @@ CurrencyDisplay.prototype.render = function () {
style: {
borderColor: inError ? 'red' : null,
},
onClick: () => this.currencyInput.focus(),
onClick: () => this.currencyInput && this.currencyInput.focus(),
}, [
h('div.currency-display__primary-row', [
h('div.currency-display__input-wrapper', [
h(CurrencyInput, {
h(readOnly ? 'input' : CurrencyInput, {
className: primaryBalanceClassName,
value: `${valueToRender}`,
placeholder: '0',
readOnly,
onInputChange: newValue => {
handleChange(this.getAmount(newValue))
},
inputRef: input => { this.currencyInput = input },
...(!readOnly ? {
onInputChange: newValue => {
handleChange(this.getAmount(newValue))
},
inputRef: input => { this.currencyInput = input },
} : {}),
}),
h('span.currency-display__currency-symbol', primaryCurrency),

View File

@ -1,8 +1,8 @@
const ethUtil = require('ethereumjs-util')
const { conversionUtil, multiplyCurrencies } = require('../../conversion-util')
const MIN_GAS_PRICE_HEX = (100000000).toString(16)
const MIN_GAS_PRICE_DEC = '100000000'
const MIN_GAS_PRICE_DEC = '0'
const MIN_GAS_PRICE_HEX = (parseInt(MIN_GAS_PRICE_DEC)).toString(16)
const MIN_GAS_LIMIT_DEC = '21000'
const MIN_GAS_LIMIT_HEX = (parseInt(MIN_GAS_LIMIT_DEC)).toString(16)

View File

@ -55,6 +55,10 @@ function ShapeshiftForm () {
}
}
ShapeshiftForm.prototype.getCoinPair = function () {
return `${this.state.depositCoin.toUpperCase()}_ETH`
}
ShapeshiftForm.prototype.componentWillMount = function () {
this.props.shapeShiftSubview()
}
@ -120,14 +124,12 @@ ShapeshiftForm.prototype.renderMetadata = function (label, value) {
}
ShapeshiftForm.prototype.renderMarketInfo = function () {
const { depositCoin } = this.state
const coinPair = `${depositCoin}_eth`
const { tokenExchangeRates } = this.props
const {
limit,
rate,
minimum,
} = tokenExchangeRates[coinPair] || {}
} = tokenExchangeRates[this.getCoinPair()] || {}
return h('div.shapeshift-form__metadata', {}, [
@ -172,10 +174,9 @@ ShapeshiftForm.prototype.renderQrCode = function () {
ShapeshiftForm.prototype.render = function () {
const { coinOptions, btnClass, warning } = this.props
const { depositCoin, errorMessage, showQrCode, depositAddress } = this.state
const coinPair = `${depositCoin}_eth`
const { errorMessage, showQrCode, depositAddress } = this.state
const { tokenExchangeRates } = this.props
const token = tokenExchangeRates[coinPair]
const token = tokenExchangeRates[this.getCoinPair()]
return h('div.shapeshift-form-wrapper', [
showQrCode

View File

@ -0,0 +1,2 @@
const Spinner = require('./spinner.component')
module.exports = Spinner

View File

@ -0,0 +1,78 @@
import React from 'react'
import PropTypes from 'prop-types'
const 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={45} y={0} rx={0} ry={0} width={10} height={30} 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={45} y={0} rx={0} ry={0} width={10} height={30} 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={45} y={0} rx={0} ry={0} width={10} height={30} 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={45} y={0} rx={0} ry={0} width={10} height={30} 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={45} y={0} rx={0} ry={0} width={10} height={30} 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={45} y={0} rx={0} ry={0} width={10} height={30} 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={45} y={0} rx={0} ry={0} width={10} height={30} 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={45} y={0} rx={0} ry={0} width={10} height={30} 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={45} y={0} rx={0} ry={0} width={10} height={30} 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={45} y={0} rx={0} ry={0} width={10} height={30} 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={45} y={0} rx={0} ry={0} width={10} height={30} 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={45} y={0} rx={0} ry={0} width={10} height={30} fill={color}>
<animate attributeName="opacity" values="1;0" dur="1s" begin="0s" repeatCount="indefinite" />
</rect>
</g>
</svg>
</div>
)
}
Spinner.propTypes = {
className: PropTypes.string,
color: PropTypes.string,
}
module.exports = Spinner

View File

@ -13,7 +13,7 @@ const SignatureRequest = require('./components/signature-request')
// const PendingMsg = require('./components/pending-msg')
// const PendingPersonalMsg = require('./components/pending-personal-msg')
// const PendingTypedMsg = require('./components/pending-typed-msg')
const Loading = require('./components/loading')
const Loading = require('./components/loading-screen')
const { DEFAULT_ROUTE } = require('./routes')
module.exports = compose(

View File

@ -36,6 +36,7 @@
font-weight: 400;
line-height: 21px;
margin-left: 8px;
cursor:pointer;
}
}

View File

@ -61,3 +61,5 @@
@import './welcome-screen.scss';
@import './sender-to-recipient.scss';
@import '../../../components/export-text-container/export-text-container.scss';

View File

@ -26,4 +26,25 @@
width: 100vw;
margin-top: 0;
}
&__container {
position: absolute;
top: 33%;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
&__message {
margin-top: 32px;
font-weight: 400;
font-size: 20px;
color: $manatee;
}
}
.spinner {
height: 58px;
width: 58px;
}

View File

@ -139,7 +139,7 @@
.network-dropdown-title {
height: 25px;
width: 75px;
width: 120px;
color: $white;
font-family: Roboto;
font-size: 18px;

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