diff --git a/packages/relay/src/index.ts b/packages/relay/src/index.ts index 8be869279..cc8472cf8 100644 --- a/packages/relay/src/index.ts +++ b/packages/relay/src/index.ts @@ -18,8 +18,8 @@ * */ -import {Block, Log, Receipt, Transaction} from './lib/model'; -import {JsonRpcError} from './lib/errors'; +import { Block, Log, Receipt, Transaction } from './lib/model'; +import { JsonRpcError } from './lib/errors/JsonRpcError'; export { JsonRpcError }; @@ -79,7 +79,7 @@ export interface Eth { getTransactionByHash(hash: string): Promise; - getTransactionCount(address: string, blocknum: string): Promise; + getTransactionCount(address: string, blocknum: string): Promise; getTransactionReceipt(hash: string): Promise; diff --git a/packages/relay/src/lib/clients/mirrorNodeClient.ts b/packages/relay/src/lib/clients/mirrorNodeClient.ts index 2ed66ffa6..85411f6c9 100644 --- a/packages/relay/src/lib/clients/mirrorNodeClient.ts +++ b/packages/relay/src/lib/clients/mirrorNodeClient.ts @@ -19,7 +19,7 @@ */ import Axios, { AxiosInstance } from 'axios'; -import { predefined } from '../errors'; +import { MirrorNodeClientError } from './../errors/MirrorNodeClientError'; import { Logger } from "pino"; import constants from './../constants'; import { Histogram, Registry } from 'prom-client'; @@ -167,7 +167,7 @@ export class MirrorNodeClient { } this.logger.error(new Error(error.message), `[GET] ${path} ${effectiveStatusCode} status`); - throw predefined.INTERNAL_ERROR; + throw new MirrorNodeClientError(error.message, effectiveStatusCode); } public async getAccountLatestTransactionByAddress(idOrAliasOrEvmAddress: string): Promise { diff --git a/packages/relay/src/lib/clients/sdkClient.ts b/packages/relay/src/lib/clients/sdkClient.ts index a5a2b4526..388fa5f7a 100644 --- a/packages/relay/src/lib/clients/sdkClient.ts +++ b/packages/relay/src/lib/clients/sdkClient.ts @@ -45,6 +45,7 @@ import { BigNumber } from '@hashgraph/sdk/lib/Transfer'; import { Logger } from "pino"; import { Gauge, Histogram, Registry } from 'prom-client'; import constants from './../constants'; +import { SDKClientError } from './../errors/SDKClientError'; const _ = require('lodash'); @@ -165,7 +166,7 @@ export class SDKClient { async getTinyBarGasFee(callerName: string): Promise { const feeSchedules = await this.getFeeSchedule(callerName); if (_.isNil(feeSchedules.current) || feeSchedules.current?.transactionFeeSchedule === undefined) { - throw new Error('Invalid FeeSchedules proto format'); + throw new SDKClientError({}, 'Invalid FeeSchedules proto format'); } for (const schedule of feeSchedules.current?.transactionFeeSchedule) { @@ -177,7 +178,7 @@ export class SDKClient { } } - throw new Error(`${constants.ETH_FUNCTIONALITY_CODE} code not found in feeSchedule`); + throw new SDKClientError({}, `${constants.ETH_FUNCTIONALITY_CODE} code not found in feeSchedule`); } async getFileIdBytes(address: string, callerName: string): Promise { @@ -252,20 +253,18 @@ export class SDKClient { return resp; } catch (e: any) { - const statusCode = e.status ? e.status._code : Status.Unknown._code; - this.logger.debug(`Consensus Node query response: ${query.constructor.name} ${statusCode}`); - this.captureMetrics( - SDKClient.queryMode, - query.constructor.name, - e.status, - query._queryPayment?.toTinybars().toNumber(), - callerName); - - if (e.status && e.status._code) { - throw new Error(e.message); + const sdkClientError = new SDKClientError(e); + if(sdkClientError.isValidNetworkError()) { + this.logger.debug(`Consensus Node query response: ${query.constructor.name} ${sdkClientError.statusCode}`); + this.captureMetrics( + SDKClient.queryMode, + query.constructor.name, + sdkClientError.status, + query._queryPayment?.toTinybars().toNumber(), + callerName); } - throw e; + throw sdkClientError; } }; @@ -278,28 +277,25 @@ export class SDKClient { return resp; } catch (e: any) { - const statusCode = e.status ? e.status._code : Status.Unknown._code; - this.logger.info(`Consensus Node ${transactionType} transaction response: ${statusCode}`); - this.captureMetrics( - SDKClient.transactionMode, - transactionType, - statusCode, - 0, - callerName); - - // capture sdk transaction response errors and shorten familiar stack trace - if (e.status && e.status._code) { - throw new Error(e.message); + const sdkClientError = new SDKClientError(e); + if(sdkClientError.isValidNetworkError()) { + this.logger.info(`Consensus Node ${transactionType} transaction response: ${sdkClientError.statusCode}`); + this.captureMetrics( + SDKClient.transactionMode, + transactionType, + sdkClientError.statusCode, + 0, + callerName); } - throw e; + throw sdkClientError; } }; executeGetTransactionRecord = async (resp: TransactionResponse, transactionName: string, callerName: string): Promise => { try { if (!resp.getRecord) { - throw new Error(`Invalid response format, expected record availability: ${JSON.stringify(resp)}`); + throw new SDKClientError({}, `Invalid response format, expected record availability: ${JSON.stringify(resp)}`); } const transactionRecord: TransactionRecord = await resp.getRecord(this.clientMain); @@ -314,18 +310,17 @@ export class SDKClient { } catch (e: any) { // capture sdk record retrieval errors and shorten familiar stack trace - if (e.status && e.status._code) { + const sdkClientError = new SDKClientError(e); + if(sdkClientError.isValidNetworkError()) { this.captureMetrics( SDKClient.transactionMode, transactionName, - e.status, + sdkClientError.status, 0, callerName); - - throw new Error(e.message); } - throw e; + throw sdkClientError; } }; @@ -353,7 +348,7 @@ export class SDKClient { private static HbarToWeiBar(balance: AccountBalance): BigNumber { return balance.hbars - .to(HbarUnit.Tinybar) - .multipliedBy(constants.TINYBAR_TO_WEIBAR_COEF); + .to(HbarUnit.Tinybar) + .multipliedBy(constants.TINYBAR_TO_WEIBAR_COEF); } } diff --git a/packages/relay/src/lib/errors.ts b/packages/relay/src/lib/errors/JsonRpcError.ts similarity index 100% rename from packages/relay/src/lib/errors.ts rename to packages/relay/src/lib/errors/JsonRpcError.ts diff --git a/packages/relay/src/lib/errors/MirrorNodeClientError.ts b/packages/relay/src/lib/errors/MirrorNodeClientError.ts new file mode 100644 index 000000000..887819d2b --- /dev/null +++ b/packages/relay/src/lib/errors/MirrorNodeClientError.ts @@ -0,0 +1,32 @@ + +/*- + * + * Hedera JSON RPC Relay + * + * Copyright (C) 2022 Hedera Hashgraph, LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +export class MirrorNodeClientError extends Error { + public statusCode: number; + + constructor(message: string, statusCode: number) { + super(message); + this.statusCode = statusCode; + + Object.setPrototypeOf(this, MirrorNodeClientError.prototype); + } + } + \ No newline at end of file diff --git a/packages/relay/src/lib/errors/SDKClientError.ts b/packages/relay/src/lib/errors/SDKClientError.ts new file mode 100644 index 000000000..35138195e --- /dev/null +++ b/packages/relay/src/lib/errors/SDKClientError.ts @@ -0,0 +1,55 @@ +/*- + * + * Hedera JSON RPC Relay + * + * Copyright (C) 2022 Hedera Hashgraph, LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +import { Status } from "@hashgraph/sdk"; + +export class SDKClientError extends Error { + public status: Status = Status.Unknown; + private validNetworkError: boolean = false; + + constructor(e: any, message?: string) { + super(e && e.status && e.status._code ? e.message : message); + + if(e && e.status && e.status._code) { + this.validNetworkError = true; + this.status = e.status; + } + + Object.setPrototypeOf(this, SDKClientError.prototype); + } + + get statusCode(): number { + return this.status._code; + } + + public isValidNetworkError(): boolean { + return this.validNetworkError; + } + + public isInvalidAccountId(): boolean { + return this.isValidNetworkError() && this.statusCode === Status.InvalidAccountId._code; + } + + public isInvalidContractId(): boolean { + return this.isValidNetworkError() && + (this.statusCode === Status.InvalidContractId._code || this.message?.includes(Status.InvalidContractId.toString())); + } +} + \ No newline at end of file diff --git a/packages/relay/src/lib/eth.ts b/packages/relay/src/lib/eth.ts index 16a2a1ce2..3bc327645 100644 --- a/packages/relay/src/lib/eth.ts +++ b/packages/relay/src/lib/eth.ts @@ -19,12 +19,13 @@ */ import { Eth } from '../index'; -import { ContractId, Status, Hbar, EthereumTransaction } from '@hashgraph/sdk'; +import { ContractId, Hbar, EthereumTransaction } from '@hashgraph/sdk'; import { BigNumber } from '@hashgraph/sdk/lib/Transfer'; import { Logger } from 'pino'; import { Block, Transaction, Log } from './model'; import { MirrorNodeClient, SDKClient } from './clients'; -import { JsonRpcError, predefined } from './errors'; +import { JsonRpcError, predefined } from './errors/JsonRpcError'; +import { SDKClientError } from './errors/SDKClientError'; import constants from './constants'; import { Precheck } from './precheck'; @@ -454,11 +455,13 @@ export class EthImpl implements Eth { return EthImpl.numberTo0x(weibars); } catch (e: any) { - // handle INVALID_ACCOUNT_ID - if (e?.status?._code === Status.InvalidAccountId._code) { - this.logger.debug(`Unable to find account ${account} in block ${JSON.stringify(blockNumber)}(${blockNumberOrTag}), returning 0x0 balance`); - cache.set(cachedLabel, EthImpl.zeroHex, constants.CACHE_TTL.ONE_HOUR); - return EthImpl.zeroHex; + if(e instanceof SDKClientError) { + // handle INVALID_ACCOUNT_ID + if (e.isInvalidAccountId()) { + this.logger.debug(`Unable to find account ${account} in block ${JSON.stringify(blockNumber)}(${blockNumberOrTag}), returning 0x0 balance`); + cache.set(cachedLabel, EthImpl.zeroHex, constants.CACHE_TTL.ONE_HOUR); + return EthImpl.zeroHex; + } } this.logger.error(e, 'Error raised during getBalance for account %s', account); @@ -486,14 +489,18 @@ export class EthImpl implements Eth { const bytecode = await this.sdkClient.getContractByteCode(0, 0, address, EthImpl.ethGetCode); return EthImpl.prepend0x(Buffer.from(bytecode).toString('hex')); } catch (e: any) { - // handle INVALID_CONTRACT_ID - if (e?.status?._code === Status.InvalidContractId._code || e?.message?.includes(Status.InvalidContractId.toString())) { - this.logger.debug('Unable to find code for contract %s in block "%s", returning 0x0, err code: %s', address, blockNumber, e?.status?._code); - cache.set(cachedLabel, '0x0', constants.CACHE_TTL.ONE_HOUR); - return '0x0'; + if(e instanceof SDKClientError) { + // handle INVALID_CONTRACT_ID + if (e.isInvalidContractId()) { + this.logger.debug('Unable to find code for contract %s in block "%s", returning 0x0, err code: %s', address, blockNumber, e.statusCode); + cache.set(cachedLabel, '0x0', constants.CACHE_TTL.ONE_HOUR); + return '0x0'; + } + this.logger.error(e, 'Error raised during getCode for address %s, err code: %s', address, e.statusCode); + } else { + this.logger.error(e, 'Error raised during getCode for address %s', address); } - this.logger.error(e, 'Error raised during getCode for address %s, err code: %s', address, e?.status?._code); throw e; } } @@ -614,19 +621,24 @@ export class EthImpl implements Eth { * @param address * @param blockNumOrTag */ - async getTransactionCount(address: string, blockNumOrTag: string): Promise { + async getTransactionCount(address: string, blockNumOrTag: string): Promise { this.logger.trace('getTransactionCount(address=%s, blockNumOrTag=%s)', address, blockNumOrTag); const blockNumber = await this.translateBlockTag(blockNumOrTag); if (blockNumber === 0) { return '0x0'; } else { - const result = await this.mirrorNodeClient.resolveEntityType(address); - if (result?.type === constants.TYPE_ACCOUNT) { - const accountInfo = await this.sdkClient.getAccountInfo(result?.entity.account, EthImpl.ethGetTransactionCount); - return EthImpl.numberTo0x(Number(accountInfo.ethereumNonce)); - } - else if (result?.type === constants.TYPE_CONTRACT) { - return EthImpl.numberTo0x(1); + try { + const result = await this.mirrorNodeClient.resolveEntityType(address); + if (result?.type === constants.TYPE_ACCOUNT) { + const accountInfo = await this.sdkClient.getAccountInfo(result?.entity.account, EthImpl.ethGetTransactionCount); + return EthImpl.numberTo0x(Number(accountInfo.ethereumNonce)); + } + else if (result?.type === constants.TYPE_CONTRACT) { + return EthImpl.numberTo0x(1); + } + } catch (e: any) { + this.logger.error(e, 'Error raised during getTransactionCount for address %s, block number or tag %s', address, blockNumOrTag); + return predefined.INTERNAL_ERROR; } return EthImpl.numberTo0x(0); @@ -711,13 +723,7 @@ export class EthImpl implements Eth { // FIXME Is this right? Maybe so? return EthImpl.prepend0x(Buffer.from(contractCallResponse.asBytes()).toString('hex')); } catch (e: any) { - // handle client error - let resolvedError = e; - if (e.status && e.status._code) { - resolvedError = new Error(e.message); - } - - this.logger.error(resolvedError, 'Failed to successfully submit contractCallQuery'); + this.logger.error(e, 'Failed to successfully submit contractCallQuery'); return predefined.INTERNAL_ERROR; } } diff --git a/packages/relay/src/lib/precheck.ts b/packages/relay/src/lib/precheck.ts index 834b27397..dcf0279b4 100644 --- a/packages/relay/src/lib/precheck.ts +++ b/packages/relay/src/lib/precheck.ts @@ -19,7 +19,7 @@ */ import * as ethers from 'ethers'; -import { predefined } from './errors'; +import { predefined } from './errors/JsonRpcError'; import { MirrorNodeClient, SDKClient } from './clients'; import { EthImpl } from './eth'; import { Logger } from 'pino'; diff --git a/packages/relay/tests/lib/eth.spec.ts b/packages/relay/tests/lib/eth.spec.ts index 671c0e3d6..de381462a 100644 --- a/packages/relay/tests/lib/eth.spec.ts +++ b/packages/relay/tests/lib/eth.spec.ts @@ -36,6 +36,8 @@ import pino from 'pino'; import { Block, Transaction } from '../../src/lib/model'; import constants from '../../src/lib/constants'; import { SDKClient } from '../../src/lib/clients'; +import { SDKClientError } from '../../src/lib/errors/SDKClientError'; + const logger = pino(); const registry = new Registry(); const Relay = new RelayImpl(logger, registry); @@ -902,11 +904,10 @@ describe('Eth calls using MirrorNode', async function () { mock.onGet(`accounts/${contractAddress1}`).reply(200, { account: contractAddress1 }); - sdkClientStub.getAccountBalanceInWeiBar.throws({ - status: { + sdkClientStub.getAccountBalanceInWeiBar.throws(new SDKClientError( + {status: { _code: 15 - } - }); + }})); const resNoCache = await ethImpl.getBalance(contractAddress1, null); const resCached = await ethImpl.getBalance(contractAddress1, null); @@ -949,11 +950,9 @@ describe('Eth calls using MirrorNode', async function () { describe('eth_getCode', async function() { it('should return cached value', async () => { - sdkClientStub.getContractByteCode.throws({ - status: { - _code: 16 - } - }); + sdkClientStub.getContractByteCode.throws(new SDKClientError({status: { + _code: 16 + }})); const resNoCache = await ethImpl.getCode(contractAddress1, null); const resCached = await ethImpl.getCode(contractAddress1, null); diff --git a/packages/server/tests/acceptance/rpc.spec.ts b/packages/server/tests/acceptance/rpc.spec.ts index 31233eb84..6d5b54543 100644 --- a/packages/server/tests/acceptance/rpc.spec.ts +++ b/packages/server/tests/acceptance/rpc.spec.ts @@ -30,7 +30,7 @@ import { AccountBalanceQuery, ContractFunctionParameters } from '@hashgraph/sdk' import parentContractJson from '../contracts/Parent.json'; import basicContractJson from '../contracts/Basic.json'; import logsContractJson from '../contracts/Logs.json'; -import { predefined } from '../../../relay/src/lib/errors'; +import { predefined } from '../../../relay/src/lib/errors/JsonRpcError'; describe('RPC Server Acceptance Tests', function () { this.timeout(240 * 1000); // 240 seconds diff --git a/packages/server/tests/clients/relayClient.ts b/packages/server/tests/clients/relayClient.ts index c1a1fae74..29835c741 100644 --- a/packages/server/tests/clients/relayClient.ts +++ b/packages/server/tests/clients/relayClient.ts @@ -21,7 +21,7 @@ import { ethers, providers } from 'ethers'; import { Logger } from 'pino'; import Assertions from '../helpers/assertions'; -import { predefined } from '../../../relay/src/lib/errors'; +import { predefined } from '../../../relay/src/lib/errors/JsonRpcError'; export default class RelayClient { diff --git a/packages/server/tests/helpers/assertions.ts b/packages/server/tests/helpers/assertions.ts index 2ec4100ba..089266a8b 100644 --- a/packages/server/tests/helpers/assertions.ts +++ b/packages/server/tests/helpers/assertions.ts @@ -19,7 +19,7 @@ */ import { expect } from 'chai'; import { ethers, BigNumber } from 'ethers'; -import { JsonRpcError, predefined } from '../../../relay/src/lib/errors'; +import { JsonRpcError, predefined } from '../../../relay/src/lib/errors/JsonRpcError'; import { Utils } from './utils'; export default class Assertions {