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 type TSetTokenBalances = typeof setTokenBalances;
|
||||||
export function setTokenBalances(payload: {
|
export function setTokenBalances(payload: {
|
||||||
[key: string]: TokenValue;
|
[key: string]: {
|
||||||
|
balance: TokenValue;
|
||||||
|
error: string | null;
|
||||||
|
};
|
||||||
}): types.SetTokenBalancesAction {
|
}): types.SetTokenBalancesAction {
|
||||||
return {
|
return {
|
||||||
type: TypeKeys.WALLET_SET_TOKEN_BALANCES,
|
type: TypeKeys.WALLET_SET_TOKEN_BALANCES,
|
||||||
|
|
|
@ -48,7 +48,10 @@ export interface SetBalanceRejectedAction {
|
||||||
export interface SetTokenBalancesAction {
|
export interface SetTokenBalancesAction {
|
||||||
type: TypeKeys.WALLET_SET_TOKEN_BALANCES;
|
type: TypeKeys.WALLET_SET_TOKEN_BALANCES;
|
||||||
payload: {
|
payload: {
|
||||||
[key: string]: TokenValue;
|
[key: string]: {
|
||||||
|
balance: TokenValue;
|
||||||
|
error: string | null;
|
||||||
|
};
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -3,8 +3,8 @@ import { checkHttpStatus, parseJSON } from './utils';
|
||||||
|
|
||||||
export function getAllRates() {
|
export function getAllRates() {
|
||||||
const mappedRates = {};
|
const mappedRates = {};
|
||||||
return _getAllRates().then((bityRates) => {
|
return _getAllRates().then(bityRates => {
|
||||||
bityRates.objects.forEach((each) => {
|
bityRates.objects.forEach(each => {
|
||||||
const pairName = each.pair;
|
const pairName = each.pair;
|
||||||
mappedRates[pairName] = parseFloat(each.rate_we_sell);
|
mappedRates[pairName] = parseFloat(each.rate_we_sell);
|
||||||
});
|
});
|
||||||
|
@ -26,7 +26,7 @@ export function postOrder(
|
||||||
mode,
|
mode,
|
||||||
pair
|
pair
|
||||||
}),
|
}),
|
||||||
headers: bityConfig.postConfig.headers
|
headers: new Headers(bityConfig.postConfig.headers)
|
||||||
})
|
})
|
||||||
.then(checkHttpStatus)
|
.then(checkHttpStatus)
|
||||||
.then(parseJSON);
|
.then(parseJSON);
|
||||||
|
@ -38,7 +38,7 @@ export function getOrderStatus(orderId: string) {
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
orderid: orderId
|
orderid: orderId
|
||||||
}),
|
}),
|
||||||
headers: bityConfig.postConfig.headers
|
headers: new Headers(bityConfig.postConfig.headers)
|
||||||
})
|
})
|
||||||
.then(checkHttpStatus)
|
.then(checkHttpStatus)
|
||||||
.then(parseJSON);
|
.then(parseJSON);
|
||||||
|
|
|
@ -87,6 +87,8 @@ export default class EquivalentValues extends React.Component<Props, CmpState> {
|
||||||
});
|
});
|
||||||
} else if (ratesError) {
|
} else if (ratesError) {
|
||||||
valuesEl = <h5>{ratesError}</h5>;
|
valuesEl = <h5>{ratesError}</h5>;
|
||||||
|
} else if (tokenBalances && tokenBalances.length === 0) {
|
||||||
|
valuesEl = <h5>No tokens found!</h5>;
|
||||||
} else {
|
} else {
|
||||||
valuesEl = (
|
valuesEl = (
|
||||||
<div className="EquivalentValues-values-loader">
|
<div className="EquivalentValues-values-loader">
|
||||||
|
|
|
@ -55,6 +55,7 @@ export interface Token {
|
||||||
address: string;
|
address: string;
|
||||||
symbol: string;
|
symbol: string;
|
||||||
decimal: number;
|
decimal: number;
|
||||||
|
error?: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface NetworkContract {
|
export interface NetworkContract {
|
||||||
|
|
|
@ -9,8 +9,14 @@ export interface TxObj {
|
||||||
export interface INode {
|
export interface INode {
|
||||||
ping(): Promise<boolean>;
|
ping(): Promise<boolean>;
|
||||||
getBalance(address: string): Promise<Wei>;
|
getBalance(address: string): Promise<Wei>;
|
||||||
getTokenBalance(address: string, token: Token): Promise<TokenValue>;
|
getTokenBalance(
|
||||||
getTokenBalances(address: string, tokens: Token[]): Promise<TokenValue[]>;
|
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>;
|
estimateGas(tx: TransactionWithoutGas): Promise<Wei>;
|
||||||
getTransactionCount(address: string): Promise<string>;
|
getTransactionCount(address: string): Promise<string>;
|
||||||
sendRawTx(tx: string): Promise<string>;
|
sendRawTx(tx: string): Promise<string>;
|
||||||
|
|
|
@ -6,7 +6,9 @@ export default class EtherscanClient extends RPCClient {
|
||||||
public encodeRequest(request: EtherscanRequest): string {
|
public encodeRequest(request: EtherscanRequest): string {
|
||||||
const encoded = new URLSearchParams();
|
const encoded = new URLSearchParams();
|
||||||
Object.keys(request).forEach(key => {
|
Object.keys(request).forEach(key => {
|
||||||
encoded.set(key, request[key]);
|
if (request[key]) {
|
||||||
|
encoded.set(key, request[key]);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
return encoded.toString();
|
return encoded.toString();
|
||||||
}
|
}
|
||||||
|
@ -14,9 +16,9 @@ export default class EtherscanClient extends RPCClient {
|
||||||
public call = (request: EtherscanRequest): Promise<JsonRpcResponse> =>
|
public call = (request: EtherscanRequest): Promise<JsonRpcResponse> =>
|
||||||
fetch(this.endpoint, {
|
fetch(this.endpoint, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: new Headers({
|
||||||
'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8'
|
'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8'
|
||||||
},
|
}),
|
||||||
body: this.encodeRequest(request)
|
body: this.encodeRequest(request)
|
||||||
}).then(r => r.json());
|
}).then(r => r.json());
|
||||||
|
|
||||||
|
|
|
@ -15,7 +15,7 @@ export default class EtherscanRequests extends RPCRequests {
|
||||||
public sendRawTx(signedTx: string): SendRawTxRequest {
|
public sendRawTx(signedTx: string): SendRawTxRequest {
|
||||||
return {
|
return {
|
||||||
module: 'proxy',
|
module: 'proxy',
|
||||||
method: 'eth_sendRawTransaction',
|
action: 'eth_sendRawTransaction',
|
||||||
hex: signedTx
|
hex: signedTx
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -23,7 +23,7 @@ export default class EtherscanRequests extends RPCRequests {
|
||||||
public estimateGas(transaction): EstimateGasRequest {
|
public estimateGas(transaction): EstimateGasRequest {
|
||||||
return {
|
return {
|
||||||
module: 'proxy',
|
module: 'proxy',
|
||||||
method: 'eth_estimateGas',
|
action: 'eth_estimateGas',
|
||||||
to: transaction.to,
|
to: transaction.to,
|
||||||
value: transaction.value,
|
value: transaction.value,
|
||||||
data: transaction.data,
|
data: transaction.data,
|
||||||
|
|
|
@ -5,7 +5,7 @@ export interface EtherscanReqBase {
|
||||||
|
|
||||||
export interface SendRawTxRequest extends EtherscanReqBase {
|
export interface SendRawTxRequest extends EtherscanReqBase {
|
||||||
module: 'proxy';
|
module: 'proxy';
|
||||||
method: 'eth_sendRawTransaction';
|
action: 'eth_sendRawTransaction';
|
||||||
hex: string;
|
hex: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -27,7 +27,7 @@ export type GetTokenBalanceRequest = CallRequest;
|
||||||
|
|
||||||
export interface EstimateGasRequest extends EtherscanReqBase {
|
export interface EstimateGasRequest extends EtherscanReqBase {
|
||||||
module: 'proxy';
|
module: 'proxy';
|
||||||
method: 'eth_estimateGas';
|
action: 'eth_estimateGas';
|
||||||
to: string;
|
to: string;
|
||||||
value: string | number;
|
value: string | number;
|
||||||
data: string;
|
data: string;
|
||||||
|
|
|
@ -2,7 +2,7 @@ import { randomBytes } from 'crypto';
|
||||||
import RPCClient from '../rpc/client';
|
import RPCClient from '../rpc/client';
|
||||||
|
|
||||||
export default class InfuraClient extends RPCClient {
|
export default class InfuraClient extends RPCClient {
|
||||||
public id(): string {
|
public id(): number {
|
||||||
return `0x${randomBytes(5).toString('hex')}`;
|
return parseInt(randomBytes(5).toString('hex'), 16);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,7 +9,7 @@ export default class RPCClient {
|
||||||
this.headers = headers;
|
this.headers = headers;
|
||||||
}
|
}
|
||||||
|
|
||||||
public id(): string {
|
public id(): string | number {
|
||||||
return randomBytes(16).toString('hex');
|
return randomBytes(16).toString('hex');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -22,10 +22,10 @@ export default class RPCClient {
|
||||||
public call = (request: RPCRequest | any): Promise<JsonRpcResponse> => {
|
public call = (request: RPCRequest | any): Promise<JsonRpcResponse> => {
|
||||||
return fetch(this.endpoint, {
|
return fetch(this.endpoint, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: this.createHeaders({
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
...this.headers
|
...this.headers
|
||||||
},
|
}),
|
||||||
body: JSON.stringify(this.decorateRequest(request))
|
body: JSON.stringify(this.decorateRequest(request))
|
||||||
}).then(r => r.json());
|
}).then(r => r.json());
|
||||||
};
|
};
|
||||||
|
@ -33,11 +33,19 @@ export default class RPCClient {
|
||||||
public batch = (requests: RPCRequest[] | any): Promise<JsonRpcResponse[]> => {
|
public batch = (requests: RPCRequest[] | any): Promise<JsonRpcResponse[]> => {
|
||||||
return fetch(this.endpoint, {
|
return fetch(this.endpoint, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: this.createHeaders({
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
...this.headers
|
...this.headers
|
||||||
},
|
}),
|
||||||
body: JSON.stringify(requests.map(this.decorateRequest))
|
body: JSON.stringify(requests.map(this.decorateRequest))
|
||||||
}).then(r => r.json());
|
}).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 { INode, TxObj } from '../INode';
|
||||||
import RPCClient from './client';
|
import RPCClient from './client';
|
||||||
import RPCRequests from './requests';
|
import RPCRequests from './requests';
|
||||||
|
import {
|
||||||
function errorOrResult(response) {
|
isValidGetBalance,
|
||||||
if (response.error) {
|
isValidEstimateGas,
|
||||||
throw new Error(response.error.message);
|
isValidCallRequest,
|
||||||
}
|
isValidTokenBalance,
|
||||||
return response.result;
|
isValidTransactionCount,
|
||||||
}
|
isValidCurrentBlock,
|
||||||
|
isValidRawTxApi
|
||||||
|
} from '../../validators';
|
||||||
|
|
||||||
export default class RpcNode implements INode {
|
export default class RpcNode implements INode {
|
||||||
public client: RPCClient;
|
public client: RPCClient;
|
||||||
|
@ -31,78 +33,87 @@ export default class RpcNode implements INode {
|
||||||
}
|
}
|
||||||
|
|
||||||
public sendCallRequest(txObj: TxObj): Promise<string> {
|
public sendCallRequest(txObj: TxObj): Promise<string> {
|
||||||
return this.client.call(this.requests.ethCall(txObj)).then(r => {
|
return this.client
|
||||||
if (r.error) {
|
.call(this.requests.ethCall(txObj))
|
||||||
throw Error(r.error.message);
|
.then(isValidCallRequest)
|
||||||
}
|
.then(response => response.result);
|
||||||
return r.result;
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
public getBalance(address: string): Promise<Wei> {
|
public getBalance(address: string): Promise<Wei> {
|
||||||
return this.client
|
return this.client
|
||||||
.call(this.requests.getBalance(address))
|
.call(this.requests.getBalance(address))
|
||||||
.then(errorOrResult)
|
.then(isValidGetBalance)
|
||||||
.then(result => Wei(result));
|
.then(({ result }) => Wei(result));
|
||||||
}
|
}
|
||||||
|
|
||||||
public estimateGas(transaction: TransactionWithoutGas): Promise<Wei> {
|
public estimateGas(transaction: TransactionWithoutGas): Promise<Wei> {
|
||||||
return this.client
|
return this.client
|
||||||
.call(this.requests.estimateGas(transaction))
|
.call(this.requests.estimateGas(transaction))
|
||||||
.then(errorOrResult)
|
.then(isValidEstimateGas)
|
||||||
.then(result => Wei(result));
|
.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
|
return this.client
|
||||||
.call(this.requests.getTokenBalance(address, token))
|
.call(this.requests.getTokenBalance(address, token))
|
||||||
.then(response => {
|
.then(isValidTokenBalance)
|
||||||
if (response.error) {
|
.then(({ result }) => {
|
||||||
// TODO - Error handling
|
return {
|
||||||
return TokenValue('0');
|
balance: TokenValue(result),
|
||||||
}
|
error: null
|
||||||
return TokenValue(response.result);
|
};
|
||||||
});
|
})
|
||||||
|
.catch(err => ({
|
||||||
|
balance: TokenValue('0'),
|
||||||
|
error: 'Caught error:' + err
|
||||||
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
public getTokenBalances(
|
public getTokenBalances(
|
||||||
address: string,
|
address: string,
|
||||||
tokens: Token[]
|
tokens: Token[]
|
||||||
): Promise<TokenValue[]> {
|
): Promise<{ balance: TokenValue; error: string | null }[]> {
|
||||||
return this.client
|
return this.client
|
||||||
.batch(tokens.map(t => this.requests.getTokenBalance(address, t)))
|
.batch(tokens.map(t => this.requests.getTokenBalance(address, t)))
|
||||||
.then(response => {
|
.then(response =>
|
||||||
return response.map(item => {
|
response.map(item => {
|
||||||
// FIXME wrap in maybe-like
|
if (isValidTokenBalance(item)) {
|
||||||
if (item.error) {
|
return {
|
||||||
return TokenValue('0');
|
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> {
|
public getTransactionCount(address: string): Promise<string> {
|
||||||
return this.client
|
return this.client
|
||||||
.call(this.requests.getTransactionCount(address))
|
.call(this.requests.getTransactionCount(address))
|
||||||
.then(errorOrResult);
|
.then(isValidTransactionCount)
|
||||||
|
.then(({ result }) => result);
|
||||||
}
|
}
|
||||||
|
|
||||||
public getCurrentBlock(): Promise<string> {
|
public getCurrentBlock(): Promise<string> {
|
||||||
return this.client
|
return this.client
|
||||||
.call(this.requests.getCurrentBlock())
|
.call(this.requests.getCurrentBlock())
|
||||||
.then(errorOrResult)
|
.then(isValidCurrentBlock)
|
||||||
.then(result => new BN(stripHexPrefix(result)).toString());
|
.then(({ result }) => new BN(stripHexPrefix(result)).toString());
|
||||||
}
|
}
|
||||||
|
|
||||||
public sendRawTx(signedTx: string): Promise<string> {
|
public sendRawTx(signedTx: string): Promise<string> {
|
||||||
return this.client
|
return this.client
|
||||||
.call(this.requests.sendRawTx(signedTx))
|
.call(this.requests.sendRawTx(signedTx))
|
||||||
.then(response => {
|
.then(isValidRawTxApi)
|
||||||
if (response.error) {
|
.then(({ result }) => {
|
||||||
throw new Error(response.error.message);
|
return result;
|
||||||
}
|
|
||||||
return response.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 => {
|
return new Promise(resolve => {
|
||||||
this.web3.eth.call(
|
this.web3.eth.call(
|
||||||
{
|
{
|
||||||
|
@ -68,10 +74,10 @@ export default class Web3Node implements INode {
|
||||||
(err, res) => {
|
(err, res) => {
|
||||||
if (err) {
|
if (err) {
|
||||||
// TODO - Error handling
|
// TODO - Error handling
|
||||||
return resolve(TokenValue('0'));
|
return resolve({ balance: TokenValue('0'), error: err });
|
||||||
}
|
}
|
||||||
// web3 returns string
|
// web3 returns string
|
||||||
resolve(TokenValue(res));
|
resolve({ balance: TokenValue(res), error: null });
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
@ -80,11 +86,14 @@ export default class Web3Node implements INode {
|
||||||
public getTokenBalances(
|
public getTokenBalances(
|
||||||
address: string,
|
address: string,
|
||||||
tokens: Token[]
|
tokens: Token[]
|
||||||
): Promise<TokenValue[]> {
|
): Promise<{ balance: TokenValue; error: string | null }[]> {
|
||||||
return new Promise(resolve => {
|
return new Promise(resolve => {
|
||||||
const batch = this.web3.createBatch();
|
const batch = this.web3.createBatch();
|
||||||
const totalCount = tokens.length;
|
const totalCount = tokens.length;
|
||||||
const returnArr = new Array<TokenValue>(totalCount);
|
const returnArr = new Array<{
|
||||||
|
balance: TokenValue;
|
||||||
|
error: string | null;
|
||||||
|
}>(totalCount);
|
||||||
let finishCount = 0;
|
let finishCount = 0;
|
||||||
|
|
||||||
tokens.forEach((token, index) =>
|
tokens.forEach((token, index) =>
|
||||||
|
@ -104,10 +113,16 @@ export default class Web3Node implements INode {
|
||||||
function finish(index, err, res) {
|
function finish(index, err, res) {
|
||||||
if (err) {
|
if (err) {
|
||||||
// TODO - Error handling
|
// TODO - Error handling
|
||||||
returnArr[index] = TokenValue('0');
|
returnArr[index] = {
|
||||||
|
balance: TokenValue('0'),
|
||||||
|
error: err
|
||||||
|
};
|
||||||
} else {
|
} else {
|
||||||
// web3 returns string
|
// web3 returns string
|
||||||
returnArr[index] = TokenValue(res);
|
returnArr[index] = {
|
||||||
|
balance: TokenValue(res),
|
||||||
|
error: err
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
finishCount++;
|
finishCount++;
|
||||||
|
|
|
@ -2,6 +2,8 @@ import { toChecksumAddress } from 'ethereumjs-util';
|
||||||
import { RawTransaction } from 'libs/transaction';
|
import { RawTransaction } from 'libs/transaction';
|
||||||
import WalletAddressValidator from 'wallet-address-validator';
|
import WalletAddressValidator from 'wallet-address-validator';
|
||||||
import { normalise } from './ens';
|
import { normalise } from './ens';
|
||||||
|
import { Validator } from 'jsonschema';
|
||||||
|
import { JsonRpcResponse } from './nodes/rpc/types';
|
||||||
|
|
||||||
export function isValidETHAddress(address: string): boolean {
|
export function isValidETHAddress(address: string): boolean {
|
||||||
if (!address) {
|
if (!address) {
|
||||||
|
@ -177,3 +179,67 @@ export const isValidByteCode = (byteCode: string) =>
|
||||||
|
|
||||||
export const isValidAbiJson = (abiJson: string) =>
|
export const isValidAbiJson = (abiJson: string) =>
|
||||||
abiJson && abiJson.startsWith('[') && abiJson.endsWith(']');
|
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
|
// in ETH
|
||||||
balance: Balance | { wei: null };
|
balance: Balance | { wei: null };
|
||||||
tokens: {
|
tokens: {
|
||||||
[key: string]: TokenValue;
|
[key: string]: {
|
||||||
|
balance: TokenValue;
|
||||||
|
error: string | null;
|
||||||
|
};
|
||||||
};
|
};
|
||||||
transactions: BroadcastTransactionStatus[];
|
transactions: BroadcastTransactionStatus[];
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,6 +14,7 @@ export interface TokenBalance {
|
||||||
balance: TokenValue;
|
balance: TokenValue;
|
||||||
custom: boolean;
|
custom: boolean;
|
||||||
decimal: number;
|
decimal: number;
|
||||||
|
error: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type MergedToken = Token & {
|
export type MergedToken = Token & {
|
||||||
|
@ -38,8 +39,11 @@ export function getTokenBalances(state: AppState): TokenBalance[] {
|
||||||
return tokens.map(t => ({
|
return tokens.map(t => ({
|
||||||
symbol: t.symbol,
|
symbol: t.symbol,
|
||||||
balance: state.wallet.tokens[t.symbol]
|
balance: state.wallet.tokens[t.symbol]
|
||||||
? state.wallet.tokens[t.symbol]
|
? state.wallet.tokens[t.symbol].balance
|
||||||
: TokenValue('0'),
|
: TokenValue('0'),
|
||||||
|
error: state.wallet.tokens[t.symbol]
|
||||||
|
? state.wallet.tokens[t.symbol].error
|
||||||
|
: null,
|
||||||
custom: t.custom,
|
custom: t.custom,
|
||||||
decimal: t.decimal
|
decimal: t.decimal
|
||||||
}));
|
}));
|
||||||
|
|
|
@ -26,8 +26,9 @@ export default function(
|
||||||
.join(',');
|
.join(',');
|
||||||
|
|
||||||
const popup = window.open('about:blank', 'printWindow', featuresStr);
|
const popup = window.open('about:blank', 'printWindow', featuresStr);
|
||||||
popup.document.open();
|
if (popup) {
|
||||||
popup.document.write(`
|
popup.document.open();
|
||||||
|
popup.document.write(`
|
||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
<style>${options.styles}</style>
|
<style>${options.styles}</style>
|
||||||
|
@ -50,4 +51,5 @@ export default function(
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
`);
|
`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,2 +1,17 @@
|
||||||
global.fetch = require('node-fetch')
|
'use strict';
|
||||||
window.BASE_API = 'http://localhost:4000/api/v1'
|
|
||||||
|
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",
|
"font-awesome": "4.7.0",
|
||||||
"hdkey": "0.7.1",
|
"hdkey": "0.7.1",
|
||||||
"idna-uts46": "1.1.0",
|
"idna-uts46": "1.1.0",
|
||||||
|
"jsonschema": "1.2.0",
|
||||||
"lodash": "4.17.4",
|
"lodash": "4.17.4",
|
||||||
"moment": "2.19.3",
|
"moment": "2.19.3",
|
||||||
"qrcode": "1.0.0",
|
"qrcode": "1.0.0",
|
||||||
|
@ -104,6 +105,7 @@
|
||||||
"types-rlp": "0.0.1",
|
"types-rlp": "0.0.1",
|
||||||
"typescript": "2.5.2",
|
"typescript": "2.5.2",
|
||||||
"url-loader": "0.6.2",
|
"url-loader": "0.6.2",
|
||||||
|
"url-search-params-polyfill": "2.0.1",
|
||||||
"webpack": "3.8.1",
|
"webpack": "3.8.1",
|
||||||
"webpack-dev-middleware": "1.12.2",
|
"webpack-dev-middleware": "1.12.2",
|
||||||
"webpack-hot-middleware": "2.21.0"
|
"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 {
|
"client": RPCClient {
|
||||||
"batch": [Function],
|
"batch": [Function],
|
||||||
"call": [Function],
|
"call": [Function],
|
||||||
|
"createHeaders": [Function],
|
||||||
"decorateRequest": [Function],
|
"decorateRequest": [Function],
|
||||||
"endpoint": "https://api.myetherapi.com/rop",
|
"endpoint": "https://api.myetherapi.com/rop",
|
||||||
"headers": Object {},
|
"headers": Object {},
|
||||||
|
|
|
@ -60,7 +60,16 @@ describe('wallet reducer', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should handle WALLET_SET_TOKEN_BALANCES', () => {
|
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(
|
expect(
|
||||||
wallet(undefined, walletActions.setTokenBalances(tokenBalances))
|
wallet(undefined, walletActions.setTokenBalances(tokenBalances))
|
||||||
).toEqual({
|
).toEqual({
|
||||||
|
|
|
@ -178,6 +178,7 @@ Object {
|
||||||
"client": RPCClient {
|
"client": RPCClient {
|
||||||
"batch": [Function],
|
"batch": [Function],
|
||||||
"call": [Function],
|
"call": [Function],
|
||||||
|
"createHeaders": [Function],
|
||||||
"decorateRequest": [Function],
|
"decorateRequest": [Function],
|
||||||
"endpoint": "",
|
"endpoint": "",
|
||||||
"headers": Object {},
|
"headers": Object {},
|
||||||
|
@ -202,6 +203,7 @@ Object {
|
||||||
"client": RPCClient {
|
"client": RPCClient {
|
||||||
"batch": [Function],
|
"batch": [Function],
|
||||||
"call": [Function],
|
"call": [Function],
|
||||||
|
"createHeaders": [Function],
|
||||||
"decorateRequest": [Function],
|
"decorateRequest": [Function],
|
||||||
"endpoint": "",
|
"endpoint": "",
|
||||||
"headers": Object {},
|
"headers": Object {},
|
||||||
|
|
Loading…
Reference in New Issue