diff --git a/common/config/data.ts b/common/config/data.ts index 25a1ef87..78bab0fd 100644 --- a/common/config/data.ts +++ b/common/config/data.ts @@ -268,43 +268,34 @@ export const NODES: { [key: string]: NodeConfig } = { } }; -export function initWeb3Node(): Promise { - return new Promise((resolve, reject) => { - const { web3 } = window as any; +export async function initWeb3Node(): Promise { + const { web3 } = window as any; - if (!web3) { - return reject( - new Error( - 'Web3 not found. Please check that MetaMask is installed, or that MyEtherWallet is open in Mist.' - ) - ); - } + if (!web3 || !web3.currentProvider || !web3.currentProvider.sendAsync) { + throw new Error( + 'Web3 not found. Please check that MetaMask is installed, or that MyEtherWallet is open in Mist.' + ); + } - if (web3.version.network === 'loading') { - return reject( - new Error( - 'MetaMask / Mist is still loading. Please refresh the page and try again.' - ) - ); - } + const lib = new Web3Node(); + const networkId = await lib.getNetVersion(); + const accounts = await lib.getAccounts(); - web3.version.getNetwork((err, networkId) => { - if (err) { - return reject(err); - } + if (!accounts.length) { + throw new Error('No accounts found in MetaMask / Mist.'); + } - try { - NODES.web3 = { - network: networkIdToName(networkId), - service: 'MetaMask / Mist', - lib: new Web3Node(web3), - estimateGas: false, - hidden: true - }; - resolve(); - } catch (err) { - reject(err); - } - }); - }); + if (networkId === 'loading') { + throw new Error( + 'MetaMask / Mist is still loading. Please refresh the page and try again.' + ); + } + + NODES.web3 = { + network: networkIdToName(networkId), + service: 'MetaMask / Mist', + lib, + estimateGas: false, + hidden: true + }; } diff --git a/common/containers/Tabs/SendTransaction/index.tsx b/common/containers/Tabs/SendTransaction/index.tsx index e4664814..4b913983 100644 --- a/common/containers/Tabs/SendTransaction/index.tsx +++ b/common/containers/Tabs/SendTransaction/index.tsx @@ -293,8 +293,9 @@ export class SendTransaction extends React.Component { />
{/* Send Form */} + {unlocked && - !(offline || (forceOffline && isWeb3Wallet)) && ( + !((offline || forceOffline) && isWeb3Wallet) && (
{hasQueryString && ( @@ -434,7 +435,7 @@ export class SendTransaction extends React.Component { )} {unlocked && - (offline || (forceOffline && isWeb3Wallet)) && ( + ((offline || forceOffline) && isWeb3Wallet) && (

Sorry...

diff --git a/common/libs/nodes/rpc/types.ts b/common/libs/nodes/rpc/types.ts index cc5a3f7e..da63c1ad 100644 --- a/common/libs/nodes/rpc/types.ts +++ b/common/libs/nodes/rpc/types.ts @@ -1,8 +1,8 @@ // don't use flow temporarily import { TransactionWithoutGas } from 'libs/messages'; -type DATA = string; -type QUANTITY = string; +export type DATA = string; +export type QUANTITY = string; type TX = string; export type DEFAULT_BLOCK = string | 'earliest' | 'latest' | 'pending'; @@ -19,8 +19,9 @@ export interface JsonRpcResponse { }; } -interface RPCRequestBase { +export interface RPCRequestBase { method: string; + params?: any[]; } export interface SendRawTxRequest extends RPCRequestBase { @@ -74,6 +75,7 @@ export interface GetCurrentBlockRequest extends RPCRequestBase { } export type RPCRequest = + | RPCRequestBase //base added so I can add an empty params array in decorateRequest without TS complaining | GetBalanceRequest | GetTokenBalanceRequest | CallRequest diff --git a/common/libs/nodes/web3/client.ts b/common/libs/nodes/web3/client.ts new file mode 100644 index 00000000..32af32a7 --- /dev/null +++ b/common/libs/nodes/web3/client.ts @@ -0,0 +1,43 @@ +import { JsonRpcResponse, RPCRequest } from '../rpc/types'; +import { IWeb3Provider } from './types'; +import RPCClient from '../rpc/client'; + +export default class Web3Client extends RPCClient { + private provider: IWeb3Provider; + + constructor() { + super('web3'); // initialized with fake endpoint + this.provider = (window as any).web3.currentProvider; + } + + public decorateRequest = (req: RPCRequest) => ({ + ...req, + id: this.id(), + jsonrpc: '2.0', + params: req.params || [] // default to empty array so MetaMask doesn't error + }); + + public call = (request: RPCRequest | any): Promise => + this.sendAsync(this.decorateRequest(request)) as Promise; + + public batch = (requests: RPCRequest[] | any): Promise => + this.sendAsync(requests.map(this.decorateRequest)) as Promise< + JsonRpcResponse[] + >; + + private sendAsync = ( + request: any + ): Promise => { + return new Promise((resolve, reject) => { + this.provider.sendAsync( + request, + (error, result: JsonRpcResponse | JsonRpcResponse[]) => { + if (error) { + return reject(error); + } + resolve(result); + } + ); + }); + }; +} diff --git a/common/libs/nodes/web3/index.ts b/common/libs/nodes/web3/index.ts index 64d77690..caf25e94 100644 --- a/common/libs/nodes/web3/index.ts +++ b/common/libs/nodes/web3/index.ts @@ -1,170 +1,55 @@ -import { Token } from 'config/data'; -import { TransactionWithoutGas } from 'libs/messages'; -import { Wei, TokenValue } from 'libs/units'; -import { INode, TxObj } from '../INode'; -import ERC20 from 'libs/erc20'; +import RPCNode from '../rpc'; +import Web3Client from './client'; +import Web3Requests from './requests'; +import { Web3Transaction } from './types'; +import { INode } from 'libs/nodes/INode'; -export default class Web3Node implements INode { - private web3: any; +import { + isValidSendTransaction, + isValidSignMessage, + isValidGetAccounts, + isValidGetNetVersion +} from '../../validators'; - constructor(web3: any) { - this.web3 = web3; +export default class Web3Node extends RPCNode { + public client: Web3Client; + public requests: Web3Requests; + + constructor() { + super('web3'); // initialized with fake endpoint + this.client = new Web3Client(); + this.requests = new Web3Requests(); } - public ping(): Promise { - return Promise.resolve(true); + public getNetVersion(): Promise { + return this.client + .call(this.requests.getNetVersion()) + .then(isValidGetNetVersion) + .then(({ result }) => result); } - public sendCallRequest(txObj: TxObj): Promise { - return new Promise((resolve, reject) => { - this.web3.eth.call(txObj, 'pending', (err, res) => { - if (err) { - return reject(err.message); - } - // web3 return string - resolve(res); - }); - }); + public sendTransaction(web3Tx: Web3Transaction): Promise { + return this.client + .call(this.requests.sendTransaction(web3Tx)) + .then(isValidSendTransaction) + .then(({ result }) => result); } - public getBalance(address: string): Promise { - return new Promise((resolve, reject) => { - this.web3.eth.getBalance(address, (err, res) => { - if (err) { - return reject(err); - } - // web3 returns BigNumber - resolve(Wei(res.toString())); - }); - }); + public signMessage(msgHex: string, fromAddr: string): Promise { + return this.client + .call(this.requests.signMessage(msgHex, fromAddr)) + .then(isValidSignMessage) + .then(({ result }) => result); } - public estimateGas(transaction: TransactionWithoutGas): Promise { - return new Promise((resolve, reject) => - this.web3.eth.estimateGas( - { - to: transaction.to, - data: transaction.data - }, - (err, res) => { - if (err) { - return reject(err); - } - // web3 returns number - resolve(Wei(res)); - } - ) - ); - } - - public getTokenBalance( - address: string, - token: Token - ): Promise<{ - balance: TokenValue; - error: string | null; - }> { - return new Promise(resolve => { - this.web3.eth.call( - { - to: token.address, - data: ERC20.balanceOf(address) - }, - 'pending', - (err, res) => { - if (err) { - // TODO - Error handling - return resolve({ balance: TokenValue('0'), error: err }); - } - // web3 returns string - resolve({ balance: TokenValue(res), error: null }); - } - ); - }); - } - - public getTokenBalances( - address: string, - tokens: Token[] - ): Promise<{ balance: TokenValue; error: string | null }[]> { - return new Promise(resolve => { - const batch = this.web3.createBatch(); - const totalCount = tokens.length; - const returnArr = new Array<{ - balance: TokenValue; - error: string | null; - }>(totalCount); - let finishCount = 0; - - tokens.forEach((token, index) => - batch.add( - this.web3.eth.call.request( - { - to: token.address, - data: ERC20.balanceOf(address) - }, - 'pending', - (err, res) => finish(index, err, res) - ) - ) - ); - batch.execute(); - - function finish(index, err, res) { - if (err) { - // TODO - Error handling - returnArr[index] = { - balance: TokenValue('0'), - error: err - }; - } else { - // web3 returns string - returnArr[index] = { - balance: TokenValue(res), - error: err - }; - } - - finishCount++; - if (finishCount === totalCount) { - resolve(returnArr); - } - } - }); - } - - public getTransactionCount(address: string): Promise { - return new Promise((resolve, reject) => - this.web3.eth.getTransactionCount(address, 'pending', (err, txCount) => { - if (err) { - return reject(err); - } - // web3 returns number - resolve(txCount.toString()); - }) - ); - } - - public getCurrentBlock(): Promise { - return new Promise((resolve, reject) => - this.web3.eth.getBlock('latest', false, (err, block) => { - if (err) { - return reject(err); - } - resolve(block.number); - }) - ); - } - - public sendRawTx(signedTx: string): Promise { - return new Promise((resolve, reject) => - this.web3.eth.sendRawTransaction(signedTx, (err, txHash) => { - if (err) { - return reject(err); - } - // web3 return string - resolve(txHash); - }) - ); + public getAccounts(): Promise { + return this.client + .call(this.requests.getAccounts()) + .then(isValidGetAccounts) + .then(({ result }) => result); } } + +export function isWeb3Node(nodeLib: INode | Web3Node): nodeLib is Web3Node { + return nodeLib instanceof Web3Node; +} diff --git a/common/libs/nodes/web3/requests.ts b/common/libs/nodes/web3/requests.ts new file mode 100644 index 00000000..b0e5d4b8 --- /dev/null +++ b/common/libs/nodes/web3/requests.ts @@ -0,0 +1,29 @@ +import RPCRequests from '../rpc/requests'; +import { + SendTransactionRequest, + SignMessageRequest, + GetAccountsRequest, + Web3Transaction +} from './types'; + +export default class Web3Requests extends RPCRequests { + public sendTransaction(web3Tx: Web3Transaction): SendTransactionRequest { + return { + method: 'eth_sendTransaction', + params: [web3Tx] + }; + } + + public signMessage(msgHex: string, fromAddr: string): SignMessageRequest { + return { + method: 'personal_sign', + params: [msgHex, fromAddr] + }; + } + + public getAccounts(): GetAccountsRequest { + return { + method: 'eth_accounts' + }; + } +} diff --git a/common/libs/nodes/web3/types.ts b/common/libs/nodes/web3/types.ts new file mode 100644 index 00000000..330a22fb --- /dev/null +++ b/common/libs/nodes/web3/types.ts @@ -0,0 +1,57 @@ +import { + JsonRpcResponse, + RPCRequest, + RPCRequestBase, + DATA, + QUANTITY +} from '../rpc/types'; + +type MESSAGE_HEX = string; +type ADDRESS = string; + +export interface Web3Transaction { + from: string; + to: string; + value: string; + gas: string; + gasPrice: string; + data: string; + nonce: string; +} + +export interface SendTransactionRequest extends RPCRequestBase { + method: 'eth_sendTransaction'; + params: [ + { + from: DATA; + to: DATA; + gas: QUANTITY; + gasPrice: QUANTITY; + value: QUANTITY; + data?: DATA; + nonce?: QUANTITY; + } + ]; +} + +export interface SignMessageRequest extends RPCRequestBase { + method: 'personal_sign'; + params: [MESSAGE_HEX, ADDRESS]; +} + +export interface GetAccountsRequest extends RPCRequestBase { + method: 'eth_accounts'; +} + +type TWeb3ProviderCallback = ( + error, + result: JsonRpcResponse | JsonRpcResponse[] +) => any; +type TSendAsync = ( + request: RPCRequest | any, + callback: TWeb3ProviderCallback +) => void; + +export interface IWeb3Provider { + sendAsync: TSendAsync; +} diff --git a/common/libs/validators.ts b/common/libs/validators.ts index 0585b69c..9c0ae11e 100644 --- a/common/libs/validators.ts +++ b/common/libs/validators.ts @@ -197,7 +197,7 @@ export const schema = { properties: { jsonrpc: { type: 'string' }, id: { oneOf: [{ type: 'string' }, { type: 'integer' }] }, - result: { type: 'string' }, + result: { oneOf: [{ type: 'string' }, { type: 'array' }] }, status: { type: 'string' }, message: { type: 'string', maxLength: 2 } } @@ -250,3 +250,15 @@ export const isValidCurrentBlock = (response: JsonRpcResponse) => export const isValidRawTxApi = (response: JsonRpcResponse) => isValidEthCall(response, schema.RpcNode)('Raw Tx'); + +export const isValidSendTransaction = (response: JsonRpcResponse) => + isValidEthCall(response, schema.RpcNode)('Send Transaction'); + +export const isValidSignMessage = (response: JsonRpcResponse) => + isValidEthCall(response, schema.RpcNode)('Sign Message'); + +export const isValidGetAccounts = (response: JsonRpcResponse) => + isValidEthCall(response, schema.RpcNode)('Get Accounts'); + +export const isValidGetNetVersion = (response: JsonRpcResponse) => + isValidEthCall(response, schema.RpcNode)('Net Version'); diff --git a/common/libs/wallet/non-deterministic/web3.ts b/common/libs/wallet/non-deterministic/web3.ts index 2447ab97..f4521d9b 100644 --- a/common/libs/wallet/non-deterministic/web3.ts +++ b/common/libs/wallet/non-deterministic/web3.ts @@ -1,15 +1,18 @@ import { IFullWallet } from '../IWallet'; import { ExtendedRawTransaction } from 'libs/transaction'; -import { networkIdToName } from 'libs/values'; +import { networkIdToName, sanitizeHex } from 'libs/values'; import { bufferToHex } from 'ethereumjs-util'; +import { configuredStore } from 'store'; +import { getNodeLib } from 'selectors/config'; +import Web3Node, { isWeb3Node } from 'libs/nodes/web3'; +import { INode } from 'libs/nodes/INode'; +import BN from 'bn.js'; export default class Web3Wallet implements IFullWallet { - private web3: any; private address: string; private network: string; - constructor(web3: any, address: string, network: string) { - this.web3 = web3; + constructor(address: string, network: string) { this.address = address; this.network = network; } @@ -24,69 +27,53 @@ export default class Web3Wallet implements IFullWallet { ); } - public signMessage(msg: string): Promise { - return new Promise((resolve, reject) => { - const msgHex = bufferToHex(Buffer.from(msg)); - const options = { - method: 'personal_sign', - params: [msgHex, this.address], - signingAddr: this.address - }; + public async signMessage(msg: string): Promise { + const msgHex = bufferToHex(Buffer.from(msg)); + const state = configuredStore.getState(); + const nodeLib: Web3Node | INode = getNodeLib(state); - this.web3.currentProvider.sendAsync(options, (err, data) => { - if (err) { - return reject(err); - } + if (!isWeb3Node(nodeLib)) { + throw new Error('Web3 wallets can only be used with a Web3 node.'); + } - if (data.error) { - return reject(data.error); - } - resolve(data.result); - }); - }); + return nodeLib.signMessage(msgHex, this.address); } - public sendTransaction(transaction: ExtendedRawTransaction): Promise { - return new Promise((resolve, reject) => { - const { from, to, value, gasLimit, gasPrice, data, nonce } = transaction; + public async sendTransaction( + transaction: ExtendedRawTransaction + ): Promise { + const state = configuredStore.getState(); + const nodeLib: Web3Node | INode = getNodeLib(state); + const { from, to, value, gasLimit, gasPrice, data, nonce } = transaction; + const web3Tx = { + from, + to, + value, + gas: + gasLimit instanceof BN ? sanitizeHex(gasLimit.toString(16)) : gasLimit, + gasPrice: + gasPrice instanceof BN ? sanitizeHex(gasPrice.toString(16)) : gasPrice, + data, + nonce + }; - const web3Tx = { - from, - to, - value, - gas: gasLimit, - gasPrice, - data, - nonce - }; + if (!isWeb3Node(nodeLib)) { + throw new Error('Web3 wallets can only be used with a Web3 node.'); + } + await this.networkCheck(nodeLib); - // perform sanity check to ensure network hasn't changed - this.web3.version.getNetwork((err1, networkId) => { - const networkName = networkIdToName(networkId); + return nodeLib.sendTransaction(web3Tx); + } - if (err1) { - return reject(err1); - } - - if (this.network !== networkName) { - return reject( - new Error( - `Expected MetaMask / Mist network to be ${ - this.network - }, but got ${networkName}. ` + - `Please change the network or restart MyEtherWallet.` - ) - ); - } - - // execute transaction - this.web3.eth.sendTransaction(web3Tx, (err2, txHash) => { - if (err2) { - return reject(err2); - } - resolve(txHash); - }); - }); - }); + private async networkCheck(lib: Web3Node) { + const netId = await lib.getNetVersion(); + const netName = networkIdToName(netId); + if (this.network !== netName) { + throw new Error( + `Expected MetaMask / Mist network to be ${this.network}, but got ${ + netName + }. Please change the network or restart MyEtherWallet.` + ); + } } } diff --git a/common/sagas/config.ts b/common/sagas/config.ts index 0ec9bd8a..035663c1 100644 --- a/common/sagas/config.ts +++ b/common/sagas/config.ts @@ -208,7 +208,7 @@ export function* cleanCustomNetworks(): SagaIterator { } // unset web3 as the selected node if a non-web3 wallet has been selected -export function* unsetWeb3Node(action): SagaIterator { +export function* unsetWeb3NodeOnWalletEvent(action): SagaIterator { const node = yield select(getNode); const nodeConfig = yield select(getNodeConfig); const newWallet = action.payload; @@ -222,6 +222,19 @@ export function* unsetWeb3Node(action): SagaIterator { yield put(changeNodeIntent(equivalentNodeOrDefault(nodeConfig))); } +export function* unsetWeb3Node(): SagaIterator { + const node = yield select(getNode); + + if (node !== 'web3') { + return; + } + + const nodeConfig = yield select(getNodeConfig); + const newNode = equivalentNodeOrDefault(nodeConfig); + + yield put(changeNodeIntent(newNode)); +} + export const equivalentNodeOrDefault = nodeConfig => { const node = Object.keys(NODES) .filter(key => key !== 'web3') @@ -250,6 +263,7 @@ export default function* configSaga(): SagaIterator { yield takeEvery(TypeKeys.CONFIG_LANGUAGE_CHANGE, reload); yield takeEvery(TypeKeys.CONFIG_ADD_CUSTOM_NODE, switchToNewNode); yield takeEvery(TypeKeys.CONFIG_REMOVE_CUSTOM_NODE, cleanCustomNetworks); - yield takeEvery(WalletTypeKeys.WALLET_SET, unsetWeb3Node); - yield takeEvery(WalletTypeKeys.WALLET_RESET, unsetWeb3Node); + yield takeEvery(TypeKeys.CONFIG_NODE_WEB3_UNSET, unsetWeb3Node); + yield takeEvery(WalletTypeKeys.WALLET_SET, unsetWeb3NodeOnWalletEvent); + yield takeEvery(WalletTypeKeys.WALLET_RESET, unsetWeb3NodeOnWalletEvent); } diff --git a/common/sagas/wallet.tsx b/common/sagas/wallet.tsx index c3bd1e9c..a56e1145 100644 --- a/common/sagas/wallet.tsx +++ b/common/sagas/wallet.tsx @@ -13,7 +13,8 @@ import { UnlockPrivateKeyAction } from 'actions/wallet'; import { Wei } from 'libs/units'; -import { changeNodeIntent } from 'actions/config'; +import { changeNodeIntent, web3UnsetNode } from 'actions/config'; +import { TypeKeys as ConfigTypeKeys } from 'actions/config/constants'; import TransactionSucceeded from 'components/ExtendedNotifications/TransactionSucceeded'; import { INode } from 'libs/nodes/INode'; import { @@ -29,15 +30,16 @@ import { SagaIterator } from 'redux-saga'; import { apply, call, - cps, fork, put, select, - takeEvery + takeEvery, + take } from 'redux-saga/effects'; import { getNetworkConfig, getNodeLib } from 'selectors/config'; import { getTokens, getWalletInst } from 'selectors/wallet'; import translate from 'translations'; +import Web3Node, { isWeb3Node } from 'libs/nodes/web3'; export function* updateAccountBalance(): SagaIterator { try { @@ -140,32 +142,32 @@ export function* unlockMnemonic(action: UnlockMnemonicAction): SagaIterator { // inspired by v3: // https://github.com/kvhnuke/etherwallet/blob/417115b0ab4dd2033d9108a1a5c00652d38db68d/app/scripts/controllers/decryptWalletCtrl.js#L311 export function* unlockWeb3(): SagaIterator { - const failMsg1 = 'Could not connect to MetaMask / Mist.'; - const failMsg2 = 'No accounts found in MetaMask / Mist.'; - const { web3 } = window as any; - - if (!web3 || !web3.eth) { - yield put(showNotification('danger', translate(failMsg1))); - return; - } - try { yield call(initWeb3Node); + yield put(changeNodeIntent('web3')); + yield take( + action => + action.type === ConfigTypeKeys.CONFIG_NODE_CHANGE && + action.payload.nodeSelection === 'web3' + ); const network = NODES.web3.network; - const accounts = yield cps(web3.eth.getAccounts); + const nodeLib: INode | Web3Node = yield select(getNodeLib); - if (!accounts.length) { - yield put(showNotification('danger', translate(failMsg2))); - return; + if (!isWeb3Node(nodeLib)) { + throw new Error('Cannot use Web3 wallet without a Web3 node.'); } + const accounts = yield apply(nodeLib, nodeLib.getAccounts); const address = accounts[0]; - yield put(changeNodeIntent('web3')); - yield put(setWallet(new Web3Wallet(web3, address, network))); + if (!address) { + throw new Error('No accounts found in MetaMask / Mist.'); + } + yield put(setWallet(new Web3Wallet(address, network))); } catch (err) { - console.error(err); + // unset web3 node so node dropdown isn't disabled + yield put(web3UnsetNode()); yield put(showNotification('danger', translate(err.message))); } } diff --git a/spec/sagas/__snapshots__/wallet.spec.tsx.snap b/spec/sagas/__snapshots__/wallet.spec.tsx.snap index b5273d10..c7f03c87 100644 --- a/spec/sagas/__snapshots__/wallet.spec.tsx.snap +++ b/spec/sagas/__snapshots__/wallet.spec.tsx.snap @@ -194,15 +194,6 @@ Object { "payload": Web3Wallet { "address": "0xe2EdC95134bbD88443bc6D55b809F7d0C2f0C854", "network": "ETH", - "web3": Object { - "eth": Object { - "getAccounts": [Function], - }, - "network": "1", - "version": Object { - "getNetwork": [Function], - }, - }, }, "type": "WALLET_SET", }, diff --git a/spec/sagas/config.spec.ts b/spec/sagas/config.spec.ts index ba622fe3..28ecaaaa 100644 --- a/spec/sagas/config.spec.ts +++ b/spec/sagas/config.spec.ts @@ -15,6 +15,7 @@ import { handleTogglePollOfflineStatus, reload, unsetWeb3Node, + unsetWeb3NodeOnWalletEvent, equivalentNodeOrDefault } from 'sagas/config'; import { NODES } from 'config/data'; @@ -326,10 +327,42 @@ describe('handleNodeChangeIntent*', () => { }); describe('unsetWeb3Node*', () => { + const node = 'web3'; + const mockNodeConfig = { network: 'ETH' }; + const newNode = equivalentNodeOrDefault(mockNodeConfig); + const gen = unsetWeb3Node(); + + it('should select getNode', () => { + expect(gen.next().value).toEqual(select(getNode)); + }); + + it('should select getNodeConfig', () => { + expect(gen.next(node).value).toEqual(select(getNodeConfig)); + }); + + it('should put changeNodeIntent', () => { + expect(gen.next(mockNodeConfig).value).toEqual( + put(changeNodeIntent(newNode)) + ); + }); + + it('should be done', () => { + expect(gen.next().done).toEqual(true); + }); + + it('should return early if node type is not web3', () => { + const gen1 = unsetWeb3Node(); + gen1.next(); + gen1.next('notWeb3'); + expect(gen1.next().done).toEqual(true); + }); +}); + +describe('unsetWeb3NodeOnWalletEvent*', () => { const fakeAction = {}; const mockNode = 'web3'; const mockNodeConfig = { network: 'ETH' }; - const gen = unsetWeb3Node(fakeAction); + const gen = unsetWeb3NodeOnWalletEvent(fakeAction); it('should select getNode', () => { expect(gen.next().value).toEqual(select(getNode)); @@ -350,18 +383,17 @@ describe('unsetWeb3Node*', () => { }); it('should return early if node type is not web3', () => { - const gen1 = unsetWeb3Node({ payload: false }); + const gen1 = unsetWeb3NodeOnWalletEvent({ payload: false }); gen1.next(); //getNode gen1.next('notWeb3'); //getNodeConfig expect(gen1.next().done).toEqual(true); }); it('should return early if wallet type is web3', () => { - const mockWeb3 = {}; const mockAddress = '0x0'; const mockNetwork = 'ETH'; - const mockWeb3Wallet = new Web3Wallet(mockWeb3, mockAddress, mockNetwork); - const gen2 = unsetWeb3Node({ payload: mockWeb3Wallet }); + const mockWeb3Wallet = new Web3Wallet(mockAddress, mockNetwork); + const gen2 = unsetWeb3NodeOnWalletEvent({ payload: mockWeb3Wallet }); gen2.next(); //getNode gen2.next('web3'); //getNodeConfig expect(gen2.next().done).toEqual(true); diff --git a/spec/sagas/wallet.spec.tsx b/spec/sagas/wallet.spec.tsx index 215f5758..386ffcf7 100644 --- a/spec/sagas/wallet.spec.tsx +++ b/spec/sagas/wallet.spec.tsx @@ -10,10 +10,10 @@ import { broadcastTx as broadcastTxActionGen } from 'actions/wallet'; import { Wei } from 'libs/units'; -import { changeNodeIntent } from 'actions/config'; +import { changeNodeIntent, web3UnsetNode } from 'actions/config'; import { INode } from 'libs/nodes/INode'; import { initWeb3Node, Token, N_FACTOR } from 'config/data'; -import { apply, call, cps, fork, put, select } from 'redux-saga/effects'; +import { apply, call, fork, put, select, take } from 'redux-saga/effects'; import { getNetworkConfig, getNodeLib } from 'selectors/config'; import { getTokens, getWalletInst } from 'selectors/wallet'; import { @@ -27,6 +27,11 @@ import { broadcastTx } from 'sagas/wallet'; import { PrivKeyWallet } from 'libs/wallet/non-deterministic'; +import { TypeKeys as ConfigTypeKeys } from 'actions/config/constants'; +import Web3Node from 'libs/nodes/web3'; +import { cloneableGenerator } from 'redux-saga/utils'; +import { showNotification } from 'actions/notifications'; +import translate from 'translations'; // init module configuredStore.getState(); @@ -74,7 +79,6 @@ const utcKeystore = { }; // necessary so we can later inject a mocked web3 to the window -declare var window: any; describe('updateAccountBalance*', () => { const gen1 = updateAccountBalance(); @@ -243,42 +247,102 @@ describe('unlockMnemonic*', () => { }); describe('unlockWeb3*', () => { - const gen = unlockWeb3(); + const G = global as any; + const data = {} as any; + data.gen = cloneableGenerator(unlockWeb3)(); const accounts = [address]; + const { random } = Math; + let nodeLib; - window.web3 = { - eth: { - getAccounts: jest.fn(cb => cb(undefined, accounts)) - }, - version: { - getNetwork: jest.fn(cb => cb(undefined, '1')) - }, - network: '1' - }; + function sendAsync(options, cb) { + const resp = { + id: 'id' + }; + switch (options.method) { + case 'net_version': + return cb(null, { ...resp, result: '1' }); + case 'eth_accounts': + return cb(null, { ...resp, result: JSON.stringify(accounts) }); + } + } beforeAll(async done => { + G.web3 = { + currentProvider: { + sendAsync + } + }; + nodeLib = new Web3Node(); + Math.random = () => 0.001; await initWeb3Node(); done(); }); afterAll(() => { - delete window.web3; + Math.random = random; + delete G.web3; }); it('should call initWeb3Node', () => { - expect(gen.next().value).toEqual(call(initWeb3Node)); - }); - - it('should cps web3.eth.getAccounts', () => { - expect(gen.next().value).toEqual(cps(window.web3.eth.getAccounts)); + expect(data.gen.next().value).toEqual(call(initWeb3Node)); }); it('should put changeNodeIntent', () => { - expect(gen.next(accounts).value).toEqual(put(changeNodeIntent('web3'))); + expect(data.gen.next(accounts).value).toEqual( + put(changeNodeIntent('web3')) + ); + }); + + it('should yield take on node change', () => { + const expected = take( + action => + action.type === ConfigTypeKeys.CONFIG_NODE_CHANGE && + action.payload.nodeSelection === 'web3' + ); + const result = data.gen.next().value; + expect(JSON.stringify(expected)).toEqual(JSON.stringify(result)); + }); + + it('should select getNodeLib', () => { + expect(data.gen.next().value).toEqual(select(getNodeLib)); + }); + + it('should throw & catch if node is not web3 node', () => { + data.clone = data.gen.clone(); + expect(data.clone.next().value).toEqual(put(web3UnsetNode())); + expect(data.clone.next().value).toEqual( + put( + showNotification( + 'danger', + translate('Cannot use Web3 wallet without a Web3 node.') + ) + ) + ); + expect(data.clone.next().done).toEqual(true); + }); + + it('should apply nodeLib.getAccounts', () => { + expect(data.gen.next(nodeLib).value).toEqual( + apply(nodeLib, nodeLib.getAccounts) + ); + }); + + it('should throw & catch if no accounts found', () => { + data.clone1 = data.gen.clone(); + expect(data.clone1.next([]).value).toEqual(put(web3UnsetNode())); + expect(data.clone1.next().value).toEqual( + put( + showNotification( + 'danger', + translate('No accounts found in MetaMask / Mist.') + ) + ) + ); + expect(data.clone1.next().done).toEqual(true); }); it('should match setWallet snapshot', () => { - expect(gen.next().value).toMatchSnapshot(); + expect(data.gen.next(accounts).value).toMatchSnapshot(); }); });