Web3 Future Proof, Refactor, Bugfixes (#481)

* correct web3 hide/show logic

* refactor web3 node lib

* refactor web3 wallet to use new web3 node

* update web3 node init to use new lib

* update web3 wallet saga to use new lib, address unlock flow bug

* remove comments

* add validators for web3 methods

* update web3 node to use latest standards

* remove legacy function

* update lib call, add account unlock check

* add & use new flavor of unsetWeb3Node

* address PR feedback

* add test, update tests, update snapshot
This commit is contained in:
skubakdj 2017-12-01 11:32:29 -05:00 committed by Daniel Ternyak
parent b638b746de
commit c54453729c
14 changed files with 427 additions and 317 deletions

View File

@ -268,43 +268,34 @@ export const NODES: { [key: string]: NodeConfig } = {
}
};
export function initWeb3Node(): Promise<void> {
return new Promise((resolve, reject) => {
const { web3 } = window as any;
export async function initWeb3Node(): Promise<void> {
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
};
}

View File

@ -293,8 +293,9 @@ export class SendTransaction extends React.Component<Props, State> {
/>
<div className="row">
{/* Send Form */}
{unlocked &&
!(offline || (forceOffline && isWeb3Wallet)) && (
!((offline || forceOffline) && isWeb3Wallet) && (
<main className="col-sm-8">
<div className="Tab-content-pane">
{hasQueryString && (
@ -434,7 +435,7 @@ export class SendTransaction extends React.Component<Props, State> {
)}
{unlocked &&
(offline || (forceOffline && isWeb3Wallet)) && (
((offline || forceOffline) && isWeb3Wallet) && (
<main className="col-sm-8">
<div className="Tab-content-pane">
<h4>Sorry...</h4>

View File

@ -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

View File

@ -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<JsonRpcResponse> =>
this.sendAsync(this.decorateRequest(request)) as Promise<JsonRpcResponse>;
public batch = (requests: RPCRequest[] | any): Promise<JsonRpcResponse[]> =>
this.sendAsync(requests.map(this.decorateRequest)) as Promise<
JsonRpcResponse[]
>;
private sendAsync = (
request: any
): Promise<JsonRpcResponse | JsonRpcResponse[]> => {
return new Promise((resolve, reject) => {
this.provider.sendAsync(
request,
(error, result: JsonRpcResponse | JsonRpcResponse[]) => {
if (error) {
return reject(error);
}
resolve(result);
}
);
});
};
}

View File

@ -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<boolean> {
return Promise.resolve(true);
public getNetVersion(): Promise<string> {
return this.client
.call(this.requests.getNetVersion())
.then(isValidGetNetVersion)
.then(({ result }) => result);
}
public sendCallRequest(txObj: TxObj): Promise<string> {
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<string> {
return this.client
.call(this.requests.sendTransaction(web3Tx))
.then(isValidSendTransaction)
.then(({ result }) => result);
}
public getBalance(address: string): Promise<Wei> {
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<string> {
return this.client
.call(this.requests.signMessage(msgHex, fromAddr))
.then(isValidSignMessage)
.then(({ result }) => result);
}
public estimateGas(transaction: TransactionWithoutGas): Promise<Wei> {
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<string> {
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<string> {
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<string> {
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<string> {
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;
}

View File

@ -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'
};
}
}

View File

@ -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;
}

View File

@ -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');

View File

@ -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<string> {
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<string> {
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<string> {
return new Promise((resolve, reject) => {
const { from, to, value, gasLimit, gasPrice, data, nonce } = transaction;
public async sendTransaction(
transaction: ExtendedRawTransaction
): Promise<string> {
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.`
);
}
}
}

View File

@ -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);
}

View File

@ -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)));
}
}

View File

@ -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",
},

View File

@ -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);

View File

@ -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();
});
});