From 980366694c09989b7180f4a0acba823025a06dcd Mon Sep 17 00:00:00 2001 From: Eddie Wang Date: Thu, 30 Nov 2017 00:35:17 -0500 Subject: [PATCH] RPC Error Handling (#384) * create ensureOkResponse and check against RPC responses * Merge with develop branch * added single unit test * main nodes added * getBalance method * remove console.log * minor conflict fix - readd polyfill to integration test * added two more method tests * seperate rpcnode from extended classes * fixes etherscan * added all tests * revert files with only formatting changes * remove console.logs - still need to update snapshot before tests will pass * updated snapshot due to RpcNode fixes for Infura and Etherscan nodes * added RpcNodeTest config so we don't rely on constants in code * undo formatting changes * Multiple fixes to error handling tokens. * Fixed TSC errors * Minor styling edit - change async func to promise * changed shape of tokenBalances * change balance type back to stricter TokenValue type * remove package.json change and include test for error state. * minor change removing unneeded line of code * added longer timeout for api * update snapshot --- common/actions/wallet/actionCreators.ts | 5 +- common/actions/wallet/actionTypes.ts | 5 +- common/api/bity.ts | 10 +- .../BalanceSidebar/EquivalentValues.tsx | 2 + common/config/data.ts | 1 + common/libs/nodes/INode.ts | 10 +- common/libs/nodes/etherscan/client.ts | 8 +- common/libs/nodes/etherscan/requests.ts | 4 +- common/libs/nodes/etherscan/types.ts | 4 +- common/libs/nodes/infura/client.ts | 4 +- common/libs/nodes/rpc/client.ts | 18 +++- common/libs/nodes/rpc/index.ts | 97 +++++++++++-------- common/libs/nodes/web3/index.ts | 29 ++++-- common/libs/validators.ts | 66 +++++++++++++ common/reducers/wallet.ts | 5 +- common/selectors/wallet.ts | 6 +- common/utils/printElement.ts | 6 +- jest_config/setupJest.js | 19 +++- package.json | 2 + spec/config/RpcNodeTestConfig.js | 5 + spec/config/data.spec.ts | 93 ++++++++++++++++++ .../SendTransaction.spec.tsx.snap | 1 + spec/reducers/wallet.spec.ts | 11 ++- .../deterministicWallets.spec.ts.snap | 2 + 24 files changed, 333 insertions(+), 80 deletions(-) create mode 100644 spec/config/RpcNodeTestConfig.js create mode 100644 spec/config/data.spec.ts diff --git a/common/actions/wallet/actionCreators.ts b/common/actions/wallet/actionCreators.ts index 8acf32dc..f69305f3 100644 --- a/common/actions/wallet/actionCreators.ts +++ b/common/actions/wallet/actionCreators.ts @@ -71,7 +71,10 @@ export function setBalanceRejected(): types.SetBalanceRejectedAction { export type TSetTokenBalances = typeof setTokenBalances; export function setTokenBalances(payload: { - [key: string]: TokenValue; + [key: string]: { + balance: TokenValue; + error: string | null; + }; }): types.SetTokenBalancesAction { return { type: TypeKeys.WALLET_SET_TOKEN_BALANCES, diff --git a/common/actions/wallet/actionTypes.ts b/common/actions/wallet/actionTypes.ts index 1eb64b50..e11aab99 100644 --- a/common/actions/wallet/actionTypes.ts +++ b/common/actions/wallet/actionTypes.ts @@ -48,7 +48,10 @@ export interface SetBalanceRejectedAction { export interface SetTokenBalancesAction { type: TypeKeys.WALLET_SET_TOKEN_BALANCES; payload: { - [key: string]: TokenValue; + [key: string]: { + balance: TokenValue; + error: string | null; + }; }; } diff --git a/common/api/bity.ts b/common/api/bity.ts index dc66c24d..21989135 100644 --- a/common/api/bity.ts +++ b/common/api/bity.ts @@ -3,8 +3,8 @@ import { checkHttpStatus, parseJSON } from './utils'; export function getAllRates() { const mappedRates = {}; - return _getAllRates().then((bityRates) => { - bityRates.objects.forEach((each) => { + return _getAllRates().then(bityRates => { + bityRates.objects.forEach(each => { const pairName = each.pair; mappedRates[pairName] = parseFloat(each.rate_we_sell); }); @@ -26,7 +26,7 @@ export function postOrder( mode, pair }), - headers: bityConfig.postConfig.headers + headers: new Headers(bityConfig.postConfig.headers) }) .then(checkHttpStatus) .then(parseJSON); @@ -38,7 +38,7 @@ export function getOrderStatus(orderId: string) { body: JSON.stringify({ orderid: orderId }), - headers: bityConfig.postConfig.headers + headers: new Headers(bityConfig.postConfig.headers) }) .then(checkHttpStatus) .then(parseJSON); @@ -48,4 +48,4 @@ function _getAllRates() { return fetch(`${bityConfig.bityURL}/v1/rate2/`) .then(checkHttpStatus) .then(parseJSON); -} \ No newline at end of file +} diff --git a/common/components/BalanceSidebar/EquivalentValues.tsx b/common/components/BalanceSidebar/EquivalentValues.tsx index 7fda3f81..40162333 100644 --- a/common/components/BalanceSidebar/EquivalentValues.tsx +++ b/common/components/BalanceSidebar/EquivalentValues.tsx @@ -87,6 +87,8 @@ export default class EquivalentValues extends React.Component { }); } else if (ratesError) { valuesEl =
{ratesError}
; + } else if (tokenBalances && tokenBalances.length === 0) { + valuesEl =
No tokens found!
; } else { valuesEl = (
diff --git a/common/config/data.ts b/common/config/data.ts index be69ac04..60a34757 100644 --- a/common/config/data.ts +++ b/common/config/data.ts @@ -55,6 +55,7 @@ export interface Token { address: string; symbol: string; decimal: number; + error?: string | null; } export interface NetworkContract { diff --git a/common/libs/nodes/INode.ts b/common/libs/nodes/INode.ts index 653c2c9a..b2db9a86 100644 --- a/common/libs/nodes/INode.ts +++ b/common/libs/nodes/INode.ts @@ -9,8 +9,14 @@ export interface TxObj { export interface INode { ping(): Promise; getBalance(address: string): Promise; - getTokenBalance(address: string, token: Token): Promise; - getTokenBalances(address: string, tokens: Token[]): Promise; + getTokenBalance( + address: string, + token: Token + ): Promise<{ balance: TokenValue; error: string | null }>; + getTokenBalances( + address: string, + tokens: Token[] + ): Promise<{ balance: TokenValue; error: string | null }[]>; estimateGas(tx: TransactionWithoutGas): Promise; getTransactionCount(address: string): Promise; sendRawTx(tx: string): Promise; diff --git a/common/libs/nodes/etherscan/client.ts b/common/libs/nodes/etherscan/client.ts index 77e24965..b5b666ef 100644 --- a/common/libs/nodes/etherscan/client.ts +++ b/common/libs/nodes/etherscan/client.ts @@ -6,7 +6,9 @@ export default class EtherscanClient extends RPCClient { public encodeRequest(request: EtherscanRequest): string { const encoded = new URLSearchParams(); Object.keys(request).forEach(key => { - encoded.set(key, request[key]); + if (request[key]) { + encoded.set(key, request[key]); + } }); return encoded.toString(); } @@ -14,9 +16,9 @@ export default class EtherscanClient extends RPCClient { public call = (request: EtherscanRequest): Promise => fetch(this.endpoint, { method: 'POST', - headers: { + headers: new Headers({ 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8' - }, + }), body: this.encodeRequest(request) }).then(r => r.json()); diff --git a/common/libs/nodes/etherscan/requests.ts b/common/libs/nodes/etherscan/requests.ts index 036cb60c..c98256a3 100644 --- a/common/libs/nodes/etherscan/requests.ts +++ b/common/libs/nodes/etherscan/requests.ts @@ -15,7 +15,7 @@ export default class EtherscanRequests extends RPCRequests { public sendRawTx(signedTx: string): SendRawTxRequest { return { module: 'proxy', - method: 'eth_sendRawTransaction', + action: 'eth_sendRawTransaction', hex: signedTx }; } @@ -23,7 +23,7 @@ export default class EtherscanRequests extends RPCRequests { public estimateGas(transaction): EstimateGasRequest { return { module: 'proxy', - method: 'eth_estimateGas', + action: 'eth_estimateGas', to: transaction.to, value: transaction.value, data: transaction.data, diff --git a/common/libs/nodes/etherscan/types.ts b/common/libs/nodes/etherscan/types.ts index fd153ba8..5fca1167 100644 --- a/common/libs/nodes/etherscan/types.ts +++ b/common/libs/nodes/etherscan/types.ts @@ -5,7 +5,7 @@ export interface EtherscanReqBase { export interface SendRawTxRequest extends EtherscanReqBase { module: 'proxy'; - method: 'eth_sendRawTransaction'; + action: 'eth_sendRawTransaction'; hex: string; } @@ -27,7 +27,7 @@ export type GetTokenBalanceRequest = CallRequest; export interface EstimateGasRequest extends EtherscanReqBase { module: 'proxy'; - method: 'eth_estimateGas'; + action: 'eth_estimateGas'; to: string; value: string | number; data: string; diff --git a/common/libs/nodes/infura/client.ts b/common/libs/nodes/infura/client.ts index 536173e3..2bc400ab 100644 --- a/common/libs/nodes/infura/client.ts +++ b/common/libs/nodes/infura/client.ts @@ -2,7 +2,7 @@ import { randomBytes } from 'crypto'; import RPCClient from '../rpc/client'; export default class InfuraClient extends RPCClient { - public id(): string { - return `0x${randomBytes(5).toString('hex')}`; + public id(): number { + return parseInt(randomBytes(5).toString('hex'), 16); } } diff --git a/common/libs/nodes/rpc/client.ts b/common/libs/nodes/rpc/client.ts index 144fd2cb..7f1934ee 100644 --- a/common/libs/nodes/rpc/client.ts +++ b/common/libs/nodes/rpc/client.ts @@ -9,7 +9,7 @@ export default class RPCClient { this.headers = headers; } - public id(): string { + public id(): string | number { return randomBytes(16).toString('hex'); } @@ -22,10 +22,10 @@ export default class RPCClient { public call = (request: RPCRequest | any): Promise => { return fetch(this.endpoint, { method: 'POST', - headers: { + headers: this.createHeaders({ 'Content-Type': 'application/json', ...this.headers - }, + }), body: JSON.stringify(this.decorateRequest(request)) }).then(r => r.json()); }; @@ -33,11 +33,19 @@ export default class RPCClient { public batch = (requests: RPCRequest[] | any): Promise => { return fetch(this.endpoint, { method: 'POST', - headers: { + headers: this.createHeaders({ 'Content-Type': 'application/json', ...this.headers - }, + }), body: JSON.stringify(requests.map(this.decorateRequest)) }).then(r => r.json()); }; + + private createHeaders = headerObject => { + const headers = new Headers(); + Object.keys(headerObject).forEach(name => { + headers.append(name, headerObject[name]); + }); + return headers; + }; } diff --git a/common/libs/nodes/rpc/index.ts b/common/libs/nodes/rpc/index.ts index af5acd82..005c8318 100644 --- a/common/libs/nodes/rpc/index.ts +++ b/common/libs/nodes/rpc/index.ts @@ -6,13 +6,15 @@ import { stripHexPrefix } from 'libs/values'; import { INode, TxObj } from '../INode'; import RPCClient from './client'; import RPCRequests from './requests'; - -function errorOrResult(response) { - if (response.error) { - throw new Error(response.error.message); - } - return response.result; -} +import { + isValidGetBalance, + isValidEstimateGas, + isValidCallRequest, + isValidTokenBalance, + isValidTransactionCount, + isValidCurrentBlock, + isValidRawTxApi +} from '../../validators'; export default class RpcNode implements INode { public client: RPCClient; @@ -31,78 +33,87 @@ export default class RpcNode implements INode { } public sendCallRequest(txObj: TxObj): Promise { - return this.client.call(this.requests.ethCall(txObj)).then(r => { - if (r.error) { - throw Error(r.error.message); - } - return r.result; - }); + return this.client + .call(this.requests.ethCall(txObj)) + .then(isValidCallRequest) + .then(response => response.result); } public getBalance(address: string): Promise { return this.client .call(this.requests.getBalance(address)) - .then(errorOrResult) - .then(result => Wei(result)); + .then(isValidGetBalance) + .then(({ result }) => Wei(result)); } public estimateGas(transaction: TransactionWithoutGas): Promise { return this.client .call(this.requests.estimateGas(transaction)) - .then(errorOrResult) - .then(result => Wei(result)); + .then(isValidEstimateGas) + .then(({ result }) => Wei(result)); } - public getTokenBalance(address: string, token: Token): Promise { + public getTokenBalance( + address: string, + token: Token + ): Promise<{ balance: TokenValue; error: string | null }> { return this.client .call(this.requests.getTokenBalance(address, token)) - .then(response => { - if (response.error) { - // TODO - Error handling - return TokenValue('0'); - } - return TokenValue(response.result); - }); + .then(isValidTokenBalance) + .then(({ result }) => { + return { + balance: TokenValue(result), + error: null + }; + }) + .catch(err => ({ + balance: TokenValue('0'), + error: 'Caught error:' + err + })); } public getTokenBalances( address: string, tokens: Token[] - ): Promise { + ): Promise<{ balance: TokenValue; error: string | null }[]> { return this.client .batch(tokens.map(t => this.requests.getTokenBalance(address, t))) - .then(response => { - return response.map(item => { - // FIXME wrap in maybe-like - if (item.error) { - return TokenValue('0'); + .then(response => + response.map(item => { + if (isValidTokenBalance(item)) { + return { + balance: TokenValue(item.result), + error: null + }; + } else { + return { + balance: TokenValue('0'), + error: 'Invalid object shape' + }; } - return TokenValue(item.result); - }); - }); - // TODO - Error handling + }) + ); } public getTransactionCount(address: string): Promise { return this.client .call(this.requests.getTransactionCount(address)) - .then(errorOrResult); + .then(isValidTransactionCount) + .then(({ result }) => result); } public getCurrentBlock(): Promise { return this.client .call(this.requests.getCurrentBlock()) - .then(errorOrResult) - .then(result => new BN(stripHexPrefix(result)).toString()); + .then(isValidCurrentBlock) + .then(({ result }) => new BN(stripHexPrefix(result)).toString()); } public sendRawTx(signedTx: string): Promise { return this.client .call(this.requests.sendRawTx(signedTx)) - .then(response => { - if (response.error) { - throw new Error(response.error.message); - } - return response.result; + .then(isValidRawTxApi) + .then(({ result }) => { + return result; }); } } diff --git a/common/libs/nodes/web3/index.ts b/common/libs/nodes/web3/index.ts index 47a4de75..64d77690 100644 --- a/common/libs/nodes/web3/index.ts +++ b/common/libs/nodes/web3/index.ts @@ -57,7 +57,13 @@ export default class Web3Node implements INode { ); } - public getTokenBalance(address: string, token: Token): Promise { + public getTokenBalance( + address: string, + token: Token + ): Promise<{ + balance: TokenValue; + error: string | null; + }> { return new Promise(resolve => { this.web3.eth.call( { @@ -68,10 +74,10 @@ export default class Web3Node implements INode { (err, res) => { if (err) { // TODO - Error handling - return resolve(TokenValue('0')); + return resolve({ balance: TokenValue('0'), error: err }); } // web3 returns string - resolve(TokenValue(res)); + resolve({ balance: TokenValue(res), error: null }); } ); }); @@ -80,11 +86,14 @@ export default class Web3Node implements INode { public getTokenBalances( address: string, tokens: Token[] - ): Promise { + ): Promise<{ balance: TokenValue; error: string | null }[]> { return new Promise(resolve => { const batch = this.web3.createBatch(); const totalCount = tokens.length; - const returnArr = new Array(totalCount); + const returnArr = new Array<{ + balance: TokenValue; + error: string | null; + }>(totalCount); let finishCount = 0; tokens.forEach((token, index) => @@ -104,10 +113,16 @@ export default class Web3Node implements INode { function finish(index, err, res) { if (err) { // TODO - Error handling - returnArr[index] = TokenValue('0'); + returnArr[index] = { + balance: TokenValue('0'), + error: err + }; } else { // web3 returns string - returnArr[index] = TokenValue(res); + returnArr[index] = { + balance: TokenValue(res), + error: err + }; } finishCount++; diff --git a/common/libs/validators.ts b/common/libs/validators.ts index 6fd91e7c..fff82e5e 100644 --- a/common/libs/validators.ts +++ b/common/libs/validators.ts @@ -2,6 +2,8 @@ import { toChecksumAddress } from 'ethereumjs-util'; import { RawTransaction } from 'libs/transaction'; import WalletAddressValidator from 'wallet-address-validator'; import { normalise } from './ens'; +import { Validator } from 'jsonschema'; +import { JsonRpcResponse } from './nodes/rpc/types'; export function isValidETHAddress(address: string): boolean { if (!address) { @@ -177,3 +179,67 @@ export const isValidByteCode = (byteCode: string) => export const isValidAbiJson = (abiJson: string) => abiJson && abiJson.startsWith('[') && abiJson.endsWith(']'); + +// JSONSchema Validations for Rpc responses +const v = new Validator(); + +export const schema = { + RpcNode: { + type: 'object', + additionalProperties: false, + properties: { + jsonrpc: { type: 'string' }, + id: { oneOf: [{ type: 'string' }, { type: 'integer' }] }, + result: { type: 'string' }, + status: { type: 'string' }, + message: { type: 'string', maxLength: 2 } + } + } +}; + +function isValidResult(response: JsonRpcResponse, schemaFormat): boolean { + return v.validate(response, schemaFormat).valid; +} + +function formatErrors(response: JsonRpcResponse, apiType: string) { + if (response.error) { + return `${response.error.message} ${response.error.data}`; + } + return `Invalid ${apiType} Error`; +} + +const isValidEthCall = (response: JsonRpcResponse, schemaType) => ( + apiName, + cb? +) => { + if (!isValidResult(response, schemaType)) { + if (cb) { + return cb(response); + } + throw new Error(formatErrors(response, apiName)); + } + return response; +}; + +export const isValidGetBalance = (response: JsonRpcResponse) => + isValidEthCall(response, schema.RpcNode)('Get Balance'); + +export const isValidEstimateGas = (response: JsonRpcResponse) => + isValidEthCall(response, schema.RpcNode)('Estimate Gas'); + +export const isValidCallRequest = (response: JsonRpcResponse) => + isValidEthCall(response, schema.RpcNode)('Call Request'); + +export const isValidTokenBalance = (response: JsonRpcResponse) => + isValidEthCall(response, schema.RpcNode)('Token Balance', () => ({ + result: 'Failed' + })); + +export const isValidTransactionCount = (response: JsonRpcResponse) => + isValidEthCall(response, schema.RpcNode)('Transaction Count'); + +export const isValidCurrentBlock = (response: JsonRpcResponse) => + isValidEthCall(response, schema.RpcNode)('Current Block'); + +export const isValidRawTxApi = (response: JsonRpcResponse) => + isValidEthCall(response, schema.RpcNode)('Raw Tx'); diff --git a/common/reducers/wallet.ts b/common/reducers/wallet.ts index 83a021c5..59242c46 100644 --- a/common/reducers/wallet.ts +++ b/common/reducers/wallet.ts @@ -15,7 +15,10 @@ export interface State { // in ETH balance: Balance | { wei: null }; tokens: { - [key: string]: TokenValue; + [key: string]: { + balance: TokenValue; + error: string | null; + }; }; transactions: BroadcastTransactionStatus[]; } diff --git a/common/selectors/wallet.ts b/common/selectors/wallet.ts index f2d36f5e..1e52d01b 100644 --- a/common/selectors/wallet.ts +++ b/common/selectors/wallet.ts @@ -14,6 +14,7 @@ export interface TokenBalance { balance: TokenValue; custom: boolean; decimal: number; + error: string | null; } export type MergedToken = Token & { @@ -38,8 +39,11 @@ export function getTokenBalances(state: AppState): TokenBalance[] { return tokens.map(t => ({ symbol: t.symbol, balance: state.wallet.tokens[t.symbol] - ? state.wallet.tokens[t.symbol] + ? state.wallet.tokens[t.symbol].balance : TokenValue('0'), + error: state.wallet.tokens[t.symbol] + ? state.wallet.tokens[t.symbol].error + : null, custom: t.custom, decimal: t.decimal })); diff --git a/common/utils/printElement.ts b/common/utils/printElement.ts index a1dbadef..207bfa26 100644 --- a/common/utils/printElement.ts +++ b/common/utils/printElement.ts @@ -26,8 +26,9 @@ export default function( .join(','); const popup = window.open('about:blank', 'printWindow', featuresStr); - popup.document.open(); - popup.document.write(` + if (popup) { + popup.document.open(); + popup.document.write(` @@ -50,4 +51,5 @@ export default function( `); + } } diff --git a/jest_config/setupJest.js b/jest_config/setupJest.js index 7c59e8ed..07c52109 100644 --- a/jest_config/setupJest.js +++ b/jest_config/setupJest.js @@ -1,2 +1,17 @@ -global.fetch = require('node-fetch') -window.BASE_API = 'http://localhost:4000/api/v1' +'use strict'; + +var realFetch = require('node-fetch'); +module.exports = function(url, options) { + if (/^\/\//.test(url)) { + url = 'https:' + url; + } + return realFetch.call(this, url, options); +}; + +if (!global.fetch) { + global.fetch = module.exports; + global.Response = realFetch.Response; + global.Headers = realFetch.Headers; + global.Request = realFetch.Request; +} +window.BASE_API = 'http://localhost:4000/api/v1'; diff --git a/package.json b/package.json index db241ccb..002cd484 100644 --- a/package.json +++ b/package.json @@ -20,6 +20,7 @@ "font-awesome": "4.7.0", "hdkey": "0.7.1", "idna-uts46": "1.1.0", + "jsonschema": "1.2.0", "lodash": "4.17.4", "moment": "2.19.3", "qrcode": "1.0.0", @@ -104,6 +105,7 @@ "types-rlp": "0.0.1", "typescript": "2.5.2", "url-loader": "0.6.2", + "url-search-params-polyfill": "2.0.1", "webpack": "3.8.1", "webpack-dev-middleware": "1.12.2", "webpack-hot-middleware": "2.21.0" diff --git a/spec/config/RpcNodeTestConfig.js b/spec/config/RpcNodeTestConfig.js new file mode 100644 index 00000000..998645c8 --- /dev/null +++ b/spec/config/RpcNodeTestConfig.js @@ -0,0 +1,5 @@ +module.exports = { + RpcNodes: ['eth_mew', 'etc_epool', 'etc_epool', 'rop_mew'], + EtherscanNodes: ['eth_ethscan', 'kov_ethscan', 'rin_ethscan'], + InfuraNodes: ['eth_infura', 'rop_infura', 'rin_infura'] +}; diff --git a/spec/config/data.spec.ts b/spec/config/data.spec.ts new file mode 100644 index 00000000..d02a2208 --- /dev/null +++ b/spec/config/data.spec.ts @@ -0,0 +1,93 @@ +import { NODES, NodeConfig } from '../../common/config/data'; +import { RPCNode } from '../../common/libs/nodes'; +import { Validator } from 'jsonschema'; +import { schema } from '../../common/libs/validators'; +import 'url-search-params-polyfill'; +import EtherscanNode from 'libs/nodes/etherscan'; +import InfuraNode from 'libs/nodes/infura'; +import RpcNodeTestConfig from './RpcNodeTestConfig'; + +const v = new Validator(); + +const validRequests = { + address: '0x72948fa4200d10ffaa7c594c24bbba6ef627d4a3', + transaction: { + data: '', + from: '0x72948fa4200d10ffaa7c594c24bbba6ef627d4a3', + to: '0x72948fa4200d10ffaa7c594c24bbba6ef627d4a3', + value: '0xde0b6b3a7640000' + }, + token: { + address: '0x4156d3342d5c385a87d264f90653733592000581', + symbol: 'SALT', + decimal: 8 + } +}; + +const testGetBalance = (n: RPCNode) => { + return n.client + .call(n.requests.getBalance(validRequests.address)) + .then(data => v.validate(data, schema.RpcNode)); +}; + +const testEstimateGas = (n: RPCNode) => { + return n.client + .call(n.requests.estimateGas(validRequests.transaction)) + .then(data => v.validate(data, schema.RpcNode)); +}; + +const testGetTokenBalance = (n: RPCNode) => { + const { address, token } = validRequests; + return n.client + .call(n.requests.getTokenBalance(address, token)) + .then(data => v.validate(data, schema.RpcNode)); +}; + +const RPCTests = { + getBalance: testGetBalance, + estimateGas: testEstimateGas, + getTokenBalance: testGetTokenBalance +}; + +function testRpcRequests(node: RPCNode, service: string) { + Object.keys(RPCTests).forEach(testType => { + describe(`RPC (${service}) should work`, () => { + it( + `RPC: ${testType} ${service}`, + () => { + return RPCTests[testType](node).then(d => + expect(d.valid).toBeTruthy() + ); + }, + 10000 + ); + }); + }); +} + +const mapNodeEndpoints = (nodes: { [key: string]: NodeConfig }) => { + const { RpcNodes, EtherscanNodes, InfuraNodes } = RpcNodeTestConfig; + + RpcNodes.forEach(n => { + testRpcRequests( + nodes[n].lib as RPCNode, + `${nodes[n].service} ${nodes[n].network}` + ); + }); + + EtherscanNodes.forEach(n => { + testRpcRequests( + nodes[n].lib as EtherscanNode, + `${nodes[n].service} ${nodes[n].network}` + ); + }); + + InfuraNodes.forEach(n => { + testRpcRequests( + nodes[n].lib as InfuraNode, + `${nodes[n].service} ${nodes[n].network}` + ); + }); +}; + +mapNodeEndpoints(NODES); diff --git a/spec/pages/__snapshots__/SendTransaction.spec.tsx.snap b/spec/pages/__snapshots__/SendTransaction.spec.tsx.snap index 51777a68..7bba85ab 100644 --- a/spec/pages/__snapshots__/SendTransaction.spec.tsx.snap +++ b/spec/pages/__snapshots__/SendTransaction.spec.tsx.snap @@ -56,6 +56,7 @@ exports[`render snapshot 1`] = ` "client": RPCClient { "batch": [Function], "call": [Function], + "createHeaders": [Function], "decorateRequest": [Function], "endpoint": "https://api.myetherapi.com/rop", "headers": Object {}, diff --git a/spec/reducers/wallet.spec.ts b/spec/reducers/wallet.spec.ts index a12b039a..e87d661a 100644 --- a/spec/reducers/wallet.spec.ts +++ b/spec/reducers/wallet.spec.ts @@ -60,7 +60,16 @@ describe('wallet reducer', () => { }); it('should handle WALLET_SET_TOKEN_BALANCES', () => { - const tokenBalances = { OMG: TokenValue('20') }; + const tokenBalances = { + OMG: { + balance: TokenValue('20'), + error: null + }, + WTT: { + balance: TokenValue('0'), + error: 'The request failed to execute' + } + }; expect( wallet(undefined, walletActions.setTokenBalances(tokenBalances)) ).toEqual({ diff --git a/spec/sagas/__snapshots__/deterministicWallets.spec.ts.snap b/spec/sagas/__snapshots__/deterministicWallets.spec.ts.snap index 45760707..63dc34b7 100644 --- a/spec/sagas/__snapshots__/deterministicWallets.spec.ts.snap +++ b/spec/sagas/__snapshots__/deterministicWallets.spec.ts.snap @@ -178,6 +178,7 @@ Object { "client": RPCClient { "batch": [Function], "call": [Function], + "createHeaders": [Function], "decorateRequest": [Function], "endpoint": "", "headers": Object {}, @@ -202,6 +203,7 @@ Object { "client": RPCClient { "batch": [Function], "call": [Function], + "createHeaders": [Function], "decorateRequest": [Function], "endpoint": "", "headers": Object {},