diff --git a/app/_locales/en/messages.json b/app/_locales/en/messages.json index 74bef0e7f..83f918b49 100644 --- a/app/_locales/en/messages.json +++ b/app/_locales/en/messages.json @@ -1498,6 +1498,12 @@ "usedByClients": { "message": "Used by a variety of different clients" }, + "useIn3": { + "message": "Use In3 Network" + }, + "useIn3Description": { + "message": "Substitutes Infura for In3, a decentralized alternative." + }, "userName": { "message": "Username" }, diff --git a/app/scripts/controllers/network/createIn3Client.js b/app/scripts/controllers/network/createIn3Client.js new file mode 100644 index 000000000..b275ecad8 --- /dev/null +++ b/app/scripts/controllers/network/createIn3Client.js @@ -0,0 +1,64 @@ +const mergeMiddleware = require('json-rpc-engine/src/mergeMiddleware') +const createScaffoldMiddleware = require('json-rpc-engine/src/createScaffoldMiddleware') +const createBlockReRefMiddleware = require('eth-json-rpc-middleware/block-ref') +const createRetryOnEmptyMiddleware = require('eth-json-rpc-middleware/retryOnEmpty') +const createBlockCacheMiddleware = require('eth-json-rpc-middleware/block-cache') +const createInflightMiddleware = require('eth-json-rpc-middleware/inflight-cache') +const createBlockTrackerInspectorMiddleware = require('eth-json-rpc-middleware/block-tracker-inspector') +const providerFromMiddleware = require('eth-json-rpc-middleware/providerFromMiddleware') +const createIn3Middleware = require('eth-json-rpc-in3').default +const BlockTracker = require('eth-block-tracker') + +module.exports = createIn3Client + +function createIn3Client ({ network }) { + const in3Middleware = createIn3Middleware({ chainId: network }) + const in3Provider = providerFromMiddleware(in3Middleware) + const blockTracker = new BlockTracker({ provider: in3Provider }) + + const networkMiddleware = mergeMiddleware([ + createNetworkAndChainIdMiddleware({ network }), + createBlockCacheMiddleware({ blockTracker }), + createInflightMiddleware(), + createBlockReRefMiddleware({ blockTracker, provider: in3Provider }), + createRetryOnEmptyMiddleware({ blockTracker, provider: in3Provider }), + createBlockTrackerInspectorMiddleware({ blockTracker }), + in3Middleware, + ]) + return { networkMiddleware, blockTracker } +} + +function createNetworkAndChainIdMiddleware ({ network }) { + let chainId + let netId + + switch (network) { + case 'mainnet': + netId = '1' + chainId = '0x01' + break + case 'ropsten': + netId = '3' + chainId = '0x03' + break + case 'rinkeby': + netId = '4' + chainId = '0x04' + break + case 'kovan': + netId = '42' + chainId = '0x2a' + break + case 'goerli': + netId = '5' + chainId = '0x05' + break + default: + throw new Error(`createIn3Client - unknown network "${network}"`) + } + + return createScaffoldMiddleware({ + eth_chainId: chainId, + net_version: netId, + }) +} diff --git a/app/scripts/controllers/network/enums.js b/app/scripts/controllers/network/enums.js index 472adce54..e0a084d84 100644 --- a/app/scripts/controllers/network/enums.js +++ b/app/scripts/controllers/network/enums.js @@ -17,6 +17,12 @@ const KOVAN_DISPLAY_NAME = 'Kovan' const MAINNET_DISPLAY_NAME = 'Main Ethereum Network' const GOERLI_DISPLAY_NAME = 'Goerli' +const INFURA = 'infura' +const IN3 = 'in3' +const INFURA_PROVIDER_TYPES = [ROPSTEN, RINKEBY, KOVAN, MAINNET, GOERLI] +const IN3_PROVIDER_TYPES = [KOVAN, MAINNET, GOERLI] +const RPC_PROVIDER_TYPES = [INFURA, IN3] + module.exports = { ROPSTEN, RINKEBY, @@ -34,4 +40,9 @@ module.exports = { KOVAN_DISPLAY_NAME, MAINNET_DISPLAY_NAME, GOERLI_DISPLAY_NAME, + INFURA, + IN3, + INFURA_PROVIDER_TYPES, + IN3_PROVIDER_TYPES, + RPC_PROVIDER_TYPES } diff --git a/app/scripts/controllers/network/network.js b/app/scripts/controllers/network/network.js index 7b5d8ef38..921c38965 100644 --- a/app/scripts/controllers/network/network.js +++ b/app/scripts/controllers/network/network.js @@ -8,6 +8,7 @@ const providerFromEngine = require('eth-json-rpc-middleware/providerFromEngine') const log = require('loglevel') const createMetamaskMiddleware = require('./createMetamaskMiddleware') const createInfuraClient = require('./createInfuraClient') +const createIn3Client = require ('./createIn3Client') const createJsonRpcClient = require('./createJsonRpcClient') const createLocalhostClient = require('./createLocalhostClient') const { createSwappableProxy, createEventEmitterProxy } = require('swappable-obj-proxy') @@ -15,14 +16,15 @@ const extend = require('extend') const networks = { networkList: {} } const { - ROPSTEN, - RINKEBY, - KOVAN, - MAINNET, + IN3, + INFURA, + IN3_PROVIDER_TYPES, + INFURA_PROVIDER_TYPES, LOCALHOST, - GOERLI, + MAINNET, + RINKEBY, + RPC_PROVIDER_TYPES } = require('./enums') -const INFURA_PROVIDER_TYPES = [ROPSTEN, RINKEBY, KOVAN, MAINNET, GOERLI] const env = process.env.METAMASK_ENV const METAMASK_DEBUG = process.env.METAMASK_DEBUG @@ -67,8 +69,8 @@ module.exports = class NetworkController extends EventEmitter { initializeProvider (providerParams) { this._baseProviderParams = providerParams - const { type, rpcTarget, chainId, ticker, nickname } = this.providerStore.getState() - this._configureProvider({ type, rpcTarget, chainId, ticker, nickname }) + const { type, rpcTarget, chainId, ticker, nickname, rpcPrefs, rpcType } = this.providerStore.getState() + this._configureProvider({ type, rpcTarget, chainId, ticker, nickname, rpcPrefs, rpcType }) this.lookupNetwork() } @@ -129,7 +131,7 @@ module.exports = class NetworkController extends EventEmitter { }) } - setRpcTarget (rpcTarget, chainId, ticker = 'ETH', nickname = '', rpcPrefs) { + setRpcTarget (rpcTarget, chainId, ticker = 'ETH', nickname = '', rpcPrefs = {}) { const providerConfig = { type: 'rpc', rpcTarget, @@ -141,11 +143,14 @@ module.exports = class NetworkController extends EventEmitter { this.providerConfig = providerConfig } - async setProviderType (type, rpcTarget = '', ticker = 'ETH', nickname = '') { + async setProviderType (type, rpcTarget = '', ticker = 'ETH', nickname = '', rpcPrefs = {}, rpcType = '') { assert.notEqual(type, 'rpc', `NetworkController - cannot call "setProviderType" with type 'rpc'. use "setRpcTarget"`) assert(INFURA_PROVIDER_TYPES.includes(type) || type === LOCALHOST, `NetworkController - Unknown rpc type "${type}"`) - const providerConfig = { type, rpcTarget, ticker, nickname } - this.providerConfig = providerConfig + if (!RPC_PROVIDER_TYPES.includes(rpcType)) { + rpcType = this.getProviderConfig().rpcType + } + this.providerConfig = { type, rpcTarget, ticker, nickname, rpcPrefs, rpcType } + } resetConnection () { @@ -172,12 +177,15 @@ module.exports = class NetworkController extends EventEmitter { } _configureProvider (opts) { - const { type, rpcTarget, chainId, ticker, nickname } = opts + const { type, rpcTarget, chainId, ticker, nickname, rpcPrefs, rpcType } = opts // infura type-based endpoints const isInfura = INFURA_PROVIDER_TYPES.includes(type) - if (isInfura) { - this._configureInfuraProvider(opts) - // other type-based rpc endpoints + const isIn3 = IN3_PROVIDER_TYPES.includes(type) + + if (rpcType === IN3 && IN3_PROVIDER_TYPES.includes(type)) { + this._configureIn3Provider({ type, rpcTarget, chainId, ticker, nickname, rpcPrefs, IN3 }) + } else if (INFURA_PROVIDER_TYPES.includes(type)) { + this._configureInfuraProvider({ type, rpcTarget, chainId, ticker, nickname, rpcPrefs, INFURA }) } else if (type === LOCALHOST) { this._configureLocalhostProvider() // url-based rpc endpoints @@ -202,6 +210,19 @@ module.exports = class NetworkController extends EventEmitter { this.networkConfig.putState(settings) } + _configureIn3Provider ({ type }) { + log.info('NetworkController - configureIn3Provider', type) + const networkClient = createIn3Client({ + network: type, + }) + this._setNetworkClient(networkClient) + // setup networkConfig + const settings = { + ticker: 'ETH', + } + this.networkConfig.putState(settings) + } + _configureLocalhostProvider () { log.info('NetworkController - configureLocalhostProvider') const networkClient = createLocalhostClient() diff --git a/app/scripts/controllers/preferences.js b/app/scripts/controllers/preferences.js index 1cfbb4d4c..cae7c4a84 100644 --- a/app/scripts/controllers/preferences.js +++ b/app/scripts/controllers/preferences.js @@ -17,6 +17,7 @@ class PreferencesController { * @property {object} store.accountTokens The tokens stored per account and then per network type * @property {object} store.assetImages Contains assets objects related to assets added * @property {boolean} store.useBlockie The users preference for blockie identicons within the UI + * @property {boolean} store.useIn3 True to use In3 instead of Infura * @property {boolean} store.useNonceField The users preference for nonce field within the UI * @property {object} store.featureFlags A key-boolean map, where keys refer to features and booleans to whether the * user wishes to see that feature. @@ -36,6 +37,7 @@ class PreferencesController { tokens: [], suggestedTokens: {}, useBlockie: false, + useIn3: false, useNonceField: false, // WARNING: Do not use feature flags for security-sensitive things. @@ -92,6 +94,24 @@ class PreferencesController { this.store.updateState({ useBlockie: val }) } + /** + * Getter for the `useIn3` property + */ + getUseIn3 () { + return this.store.getState().useIn3 + } + + /** + * Setter for the `useIn3` property + * + * @param {boolean} val - True to use In3 instead of Infura + * + */ + setUseIn3 (val) { + this.store.updateState({ useIn3: val }) + } + + /** * Setter for the `useNonceField` property * diff --git a/app/scripts/metamask-controller.js b/app/scripts/metamask-controller.js index 5c50a8502..204a32df9 100644 --- a/app/scripts/metamask-controller.js +++ b/app/scripts/metamask-controller.js @@ -64,6 +64,7 @@ const { PhishingController, } = require('gaba') const backEndMetaMetricsEvent = require('./lib/backend-metametrics') +const { IN3, INFURA } = require('./controllers/network/enums') module.exports = class MetamaskController extends EventEmitter { @@ -436,7 +437,6 @@ module.exports = class MetamaskController extends EventEmitter { const keyringController = this.keyringController const preferencesController = this.preferencesController const txController = this.txController - const networkController = this.networkController const providerApprovalController = this.providerApprovalController const onboardingController = this.onboardingController const threeBoxController = this.threeBoxController @@ -482,10 +482,12 @@ module.exports = class MetamaskController extends EventEmitter { submitPassword: nodeify(this.submitPassword, this), // network management - setProviderType: nodeify(networkController.setProviderType, networkController), + // setProviderType: nodeify(networkController.setProviderType, networkController), + setProviderType: this.setProvider.bind(this), setCustomRpc: nodeify(this.setCustomRpc, this), updateAndSetCustomRpc: nodeify(this.updateAndSetCustomRpc, this), delCustomRpc: nodeify(this.delCustomRpc, this), + setUseIn3Network: this.setUseIn3Network.bind(this), // PreferencesController setSelectedAddress: nodeify(preferencesController.setSelectedAddress, preferencesController), @@ -1747,6 +1749,39 @@ module.exports = class MetamaskController extends EventEmitter { await this.threeBoxController.init() } + /** + * Wrapper for networkController.setProviderType that enforces rpcType, respecting preferences selection + */ + setProvider (type, rpcTarget = '', ticker = 'ETH', nickname = '', rpcPrefs = {}, rpcType = '') { + const useIn3 = rpcType === 'in3' ? true : this.preferencesController.getUseIn3() + if (useIn3) { + this.networkController.setProviderType(type, rpcTarget, ticker, nickname, rpcPrefs, IN3) + } else { + this.networkController.setProviderType(type, rpcTarget, ticker, nickname, rpcPrefs, INFURA) + } + } + + /** + * Sets whether or not to use IN3 NEtwork provider instead of infura + * @param {boolean} val - True for IN3, false for Infura. + * @param {Function} cb - A callback function called when complete. + */ + setUseIn3Network (val, cb) { + try { + this.preferencesController.setUseIn3(val) + const cfg = this.networkController.getProviderConfig() + if (useIn3) { + this.networkController.setProviderType(cfg.type, cfg.rpcTarget, cfg.ticker, cfg.nickname, cfg.rpcPrefs, IN3) + } else { + this.networkController.setProviderType(cfg.type, cfg.rpcTarget, cfg.ticker, cfg.nickname, cfg.rpcPrefs, INFURA) + } + cb(null) + } catch (err) { + cb(err) + } + } + + /** * Sets whether or not to use the blockie identicon format. * @param {boolean} val - True for bockie, false for jazzicon. diff --git a/development/states/navigate-txs.json b/development/states/navigate-txs.json index 39d24c1ea..e6684e8d3 100644 --- a/development/states/navigate-txs.json +++ b/development/states/navigate-txs.json @@ -140,6 +140,7 @@ }, "coinOptions": {}, "useBlockie": false, + "useIn3": false, "featureFlags": { "betaUI": true, "skipAnnounceBetaUI": true diff --git a/package.json b/package.json index e3569cf79..e05eb7899 100644 --- a/package.json +++ b/package.json @@ -110,11 +110,12 @@ "eth-block-tracker": "^4.4.2", "eth-contract-metadata": "^1.12.1", "eth-ens-namehash": "^2.0.8", + "eth-hd-keyring": "github:brave/eth-hd-keyring#d811474329e1a725180d393b67d03c0f3d96106b", "eth-json-rpc-errors": "^1.1.0", "eth-json-rpc-filters": "^4.1.1", - "eth-json-rpc-middleware": "^4.4.0", - "eth-hd-keyring": "github:brave/eth-hd-keyring#d811474329e1a725180d393b67d03c0f3d96106b", + "eth-json-rpc-in3": "^0.1.9", "eth-json-rpc-infura": "github:brave/eth-json-rpc-infura#e935ea77179447d2da15082cdac913ea6bf1133d", + "eth-json-rpc-middleware": "^4.4.0", "eth-keyring-controller": "github:brave/KeyringController#9d4408c935948fb7797dd267560b4393a44c7379", "eth-method-registry": "^1.2.0", "eth-phishing-detect": "^1.1.4", diff --git a/test/data/2-state.json b/test/data/2-state.json index 9d6dc9af5..fb30a4f95 100644 --- a/test/data/2-state.json +++ b/test/data/2-state.json @@ -39,6 +39,7 @@ "currentAccountTab": "history", "tokens": [], "useBlockie": false, + "useIn3": false, "featureFlags": {}, "currentLocale": null, "identities": { diff --git a/test/unit/app/controllers/metamask-controller-test.js b/test/unit/app/controllers/metamask-controller-test.js index 7eac91a9b..89c7bfd1c 100644 --- a/test/unit/app/controllers/metamask-controller-test.js +++ b/test/unit/app/controllers/metamask-controller-test.js @@ -338,6 +338,20 @@ describe('MetaMaskController', function () { }) + describe('preferencesControllerIn3', function () { + + it('defaults useIn3 to false', function () { + assert.equal(metamaskController.preferencesController.store.getState().useIn3, false) + }) + + it('setUseIn3 to true', function () { + metamaskController.setUseIn3Network(true, noop) + assert.equal(metamaskController.preferencesController.store.getState().useIn3, true) + }) + + }) + + describe('#selectFirstIdentity', function () { let identities, address diff --git a/test/unit/migrations/035-test.js b/test/unit/migrations/035-test.js index a6ab09864..634ee9f78 100644 --- a/test/unit/migrations/035-test.js +++ b/test/unit/migrations/035-test.js @@ -68,6 +68,7 @@ describe('migration #35', () => { tokens: [], suggestedTokens: {}, useBlockie: false, + useIn3: false, knownMethodData: {}, participateInMetaMetrics: null, firstTimeFlowType: null, diff --git a/test/unit/ui/app/actions.spec.js b/test/unit/ui/app/actions.spec.js index 9ed21e729..817891784 100644 --- a/test/unit/ui/app/actions.spec.js +++ b/test/unit/ui/app/actions.spec.js @@ -1499,6 +1499,44 @@ describe('Actions', () => { }) }) + describe('#setUseIn3', () => { + let setUseIn3Spy + + beforeEach(() => { + setUseIn3Spy = sinon.stub(background, 'setUseIn3Network') + }) + + afterEach(() => { + setUseIn3Spy.restore() + }) + + it('calls setUseIn3 in background', () => { + const store = mockStore() + + store.dispatch(actions.setUseIn3()) + assert(setUseIn3Spy.calledOnce) + }) + + it('errors when setUseIn3 in background throws', () => { + const store = mockStore() + + const expectedActions = [ + { type: 'SHOW_LOADING_INDICATION', value: undefined }, + { type: 'HIDE_LOADING_INDICATION' }, + { type: 'DISPLAY_WARNING', value: 'error' }, + { type: 'SET_USE_IN3', value: undefined }, + ] + + setUseIn3Spy.callsFake((_, callback) => { + callback(new Error('error')) + }) + + store.dispatch(actions.setUseIn3()) + assert.deepEqual(store.getActions(), expectedActions) + }) + }) + + describe('#updateCurrentLocale', () => { let setCurrentLocaleSpy diff --git a/test/unit/ui/app/reducers/metamask.spec.js b/test/unit/ui/app/reducers/metamask.spec.js index 6b3dd7193..7ecf7ff7b 100644 --- a/test/unit/ui/app/reducers/metamask.spec.js +++ b/test/unit/ui/app/reducers/metamask.spec.js @@ -417,14 +417,24 @@ describe('MetaMask Reducers', () => { }) it('sets blockies', () => { + const state = reduceMetamask({}, { + type: actions.SET_USE_BLOCKIE, + value: true, + }) + + assert.equal(state.useBlockie, true) + }) + + it('sets use In3', () => { const state = reduceMetamask({}, { - type: actions.SET_USE_BLOCKIE, + type: actions.SET_USE_IN3, value: true, }) - assert.equal(state.useBlockie, true) + assert.equal(state.useIn3, true) }) + it('updates an arbitrary feature flag', () => { const state = reduceMetamask({}, { type: actions.UPDATE_FEATURE_FLAGS, diff --git a/ui/app/components/app/dropdowns/network-dropdown.js b/ui/app/components/app/dropdowns/network-dropdown.js index e6a24ef11..20d8fa571 100644 --- a/ui/app/components/app/dropdowns/network-dropdown.js +++ b/ui/app/components/app/dropdowns/network-dropdown.js @@ -69,7 +69,7 @@ module.exports = compose( // TODO: specify default props and proptypes NetworkDropdown.prototype.render = function () { const props = this.props - const { provider: { type: providerType, rpcTarget: activeNetwork }, setNetworksTabAddMode } = props + const { provider: { type: providerType, rpcTarget: activeNetwork, rpcType: rpcType }, setNetworksTabAddMode } = this.props const rpcListDetail = props.frequentRpcListDetail const isOpen = this.props.networkDropdownOpen const dropdownMenuItemStyle = { @@ -78,6 +78,10 @@ NetworkDropdown.prototype.render = function () { padding: '12px 0', } + const hidden = { + display: 'none', + } + return h(Dropdown, { isOpen, onClickOutside: (event) => { @@ -106,7 +110,6 @@ NetworkDropdown.prototype.render = function () { h('div.network-dropdown-title', {}, this.context.t('networks')), h('div.network-dropdown-divider'), - h('div.network-dropdown-content', {}, this.context.t('defaultNetwork') @@ -141,7 +144,7 @@ NetworkDropdown.prototype.render = function () { key: 'ropsten', closeMenu: () => this.props.hideNetworkDropdown(), onClick: () => this.handleClick('ropsten'), - style: dropdownMenuItemStyle, + style: rpcType === 'in3' ? hidden : dropdownMenuItemStyle, }, [ providerType === 'ropsten' ? h('i.fa.fa-check') : h('.network-check__transparent', '✓'), @@ -185,7 +188,7 @@ NetworkDropdown.prototype.render = function () { key: 'rinkeby', closeMenu: () => this.props.hideNetworkDropdown(), onClick: () => this.handleClick('rinkeby'), - style: dropdownMenuItemStyle, + style: rpcType === 'in3' ? hidden : dropdownMenuItemStyle, }, [ providerType === 'rinkeby' ? h('i.fa.fa-check') : h('.network-check__transparent', '✓'), diff --git a/ui/app/ducks/metamask/metamask.js b/ui/app/ducks/metamask/metamask.js index 8a9739af7..bc93ea51c 100644 --- a/ui/app/ducks/metamask/metamask.js +++ b/ui/app/ducks/metamask/metamask.js @@ -45,6 +45,7 @@ function reduceMetamask (state, action) { }, coinOptions: {}, useBlockie: false, + useIn3: false, featureFlags: {}, networkEndpointType: OLD_UI_NETWORK_TYPE, welcomeScreenSeen: false, @@ -367,6 +368,13 @@ function reduceMetamask (state, action) { useBlockie: action.value, }) + case actions.SET_USE_IN3: + return { + ...metamaskState, + useIn3: !metamaskState.useIn3, + } + + case actions.UPDATE_FEATURE_FLAGS: return extend(metamaskState, { featureFlags: action.value, diff --git a/ui/app/pages/settings/advanced-tab/advanced-tab.component.js b/ui/app/pages/settings/advanced-tab/advanced-tab.component.js index 5df85c027..62c153584 100644 --- a/ui/app/pages/settings/advanced-tab/advanced-tab.component.js +++ b/ui/app/pages/settings/advanced-tab/advanced-tab.component.js @@ -31,6 +31,8 @@ export default class AdvancedTab extends PureComponent { setShowFiatConversionOnTestnetsPreference: PropTypes.func.isRequired, threeBoxSyncingAllowed: PropTypes.bool.isRequired, setThreeBoxSyncingPermission: PropTypes.func.isRequired, + useIn3: PropTypes.bool, + setUseIn3: PropTypes.func.isRequired, threeBoxDisabled: PropTypes.bool.isRequired, } @@ -352,6 +354,32 @@ export default class AdvancedTab extends PureComponent { ) } + renderIn3Switch () { + const { t } = this.context + const { useIn3, setUseIn3 } = this.props + + return ( +