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
This commit is contained in:
parent
a40b22fc68
commit
980366694c
|
@ -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,
|
||||
|
|
|
@ -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;
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -87,6 +87,8 @@ export default class EquivalentValues extends React.Component<Props, CmpState> {
|
|||
});
|
||||
} else if (ratesError) {
|
||||
valuesEl = <h5>{ratesError}</h5>;
|
||||
} else if (tokenBalances && tokenBalances.length === 0) {
|
||||
valuesEl = <h5>No tokens found!</h5>;
|
||||
} else {
|
||||
valuesEl = (
|
||||
<div className="EquivalentValues-values-loader">
|
||||
|
|
|
@ -55,6 +55,7 @@ export interface Token {
|
|||
address: string;
|
||||
symbol: string;
|
||||
decimal: number;
|
||||
error?: string | null;
|
||||
}
|
||||
|
||||
export interface NetworkContract {
|
||||
|
|
|
@ -9,8 +9,14 @@ export interface TxObj {
|
|||
export interface INode {
|
||||
ping(): Promise<boolean>;
|
||||
getBalance(address: string): Promise<Wei>;
|
||||
getTokenBalance(address: string, token: Token): Promise<TokenValue>;
|
||||
getTokenBalances(address: string, tokens: Token[]): Promise<TokenValue[]>;
|
||||
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<Wei>;
|
||||
getTransactionCount(address: string): Promise<string>;
|
||||
sendRawTx(tx: string): Promise<string>;
|
||||
|
|
|
@ -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<JsonRpcResponse> =>
|
||||
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());
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<JsonRpcResponse> => {
|
||||
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<JsonRpcResponse[]> => {
|
||||
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;
|
||||
};
|
||||
}
|
||||
|
|
|
@ -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<string> {
|
||||
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<Wei> {
|
||||
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<Wei> {
|
||||
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<TokenValue> {
|
||||
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<TokenValue[]> {
|
||||
): 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<string> {
|
||||
return this.client
|
||||
.call(this.requests.getTransactionCount(address))
|
||||
.then(errorOrResult);
|
||||
.then(isValidTransactionCount)
|
||||
.then(({ result }) => result);
|
||||
}
|
||||
|
||||
public getCurrentBlock(): Promise<string> {
|
||||
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<string> {
|
||||
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;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -57,7 +57,13 @@ export default class Web3Node implements INode {
|
|||
);
|
||||
}
|
||||
|
||||
public getTokenBalance(address: string, token: Token): Promise<TokenValue> {
|
||||
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<TokenValue[]> {
|
||||
): Promise<{ balance: TokenValue; error: string | null }[]> {
|
||||
return new Promise(resolve => {
|
||||
const batch = this.web3.createBatch();
|
||||
const totalCount = tokens.length;
|
||||
const returnArr = new Array<TokenValue>(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++;
|
||||
|
|
|
@ -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');
|
||||
|
|
|
@ -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[];
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}));
|
||||
|
|
|
@ -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(`
|
||||
<html>
|
||||
<head>
|
||||
<style>${options.styles}</style>
|
||||
|
@ -50,4 +51,5 @@ export default function(
|
|||
</body>
|
||||
</html>
|
||||
`);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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']
|
||||
};
|
|
@ -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);
|
|
@ -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 {},
|
||||
|
|
|
@ -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({
|
||||
|
|
|
@ -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 {},
|
||||
|
|
Loading…
Reference in New Issue