diff --git a/packages/relay/src/lib/eth.ts b/packages/relay/src/lib/eth.ts index f25a7d894..09dd9292b 100644 --- a/packages/relay/src/lib/eth.ts +++ b/packages/relay/src/lib/eth.ts @@ -647,28 +647,14 @@ export class EthImpl implements Eth { this.logger.trace('sendRawTransaction(transaction=%s)', transaction); try { - await this.precheck.gasLimit(transaction); - await this.precheck.nonce(transaction); - this.precheck.chainId(transaction); - this.precheck.value(transaction); + const gasPrice = await this.getFeeWeibars(EthImpl.ethSendRawTransaction); + await this.precheck.sendRawTransactionCheck(transaction, gasPrice); } catch(e: any) { return e; } - const gasPrice = await this.getFeeWeibars(EthImpl.ethSendRawTransaction); - const gasPrecheck = this.precheck.gasPrice(transaction, gasPrice); - if (!gasPrecheck.passes) { - return gasPrecheck.error; - } - - const balancePrecheck = await this.precheck.balance(transaction, EthImpl.ethSendRawTransaction); - if (!balancePrecheck.passes) { - return balancePrecheck.error; - } - const transactionBuffer = Buffer.from(EthImpl.prune0x(transaction), 'hex'); - try { const contractExecuteResponse = await this.sdkClient.submitEthereumTransaction(transactionBuffer, EthImpl.ethSendRawTransaction); diff --git a/packages/relay/src/lib/precheck.ts b/packages/relay/src/lib/precheck.ts index beb14c047..834b27397 100644 --- a/packages/relay/src/lib/precheck.ts +++ b/packages/relay/src/lib/precheck.ts @@ -21,14 +21,15 @@ import * as ethers from 'ethers'; import { predefined } from './errors'; import { MirrorNodeClient, SDKClient } from './clients'; -import { EthImpl } from "./eth"; -import { Logger } from "pino"; +import { EthImpl } from './eth'; +import { Logger } from 'pino'; import constants from './constants'; +import { Transaction } from 'ethers'; export class Precheck { private mirrorNodeClient: MirrorNodeClient; private sdkClient: SDKClient; - private chain: string; + private readonly chain: string; private readonly logger: Logger; constructor(mirrorNodeClient: MirrorNodeClient, sdkClient: SDKClient, logger: Logger, chainId: string) { @@ -38,8 +39,13 @@ export class Precheck { this.logger = logger; } - value(transaction: string) { - const tx = ethers.utils.parseTransaction(transaction); + private static parseTxIfNeeded(transaction: string | Transaction): Transaction { + return typeof transaction === 'string' + ? ethers.utils.parseTransaction(transaction) + : transaction; + } + + value(tx: Transaction) { if (tx.data === EthImpl.emptyHex && tx.value.lt(constants.TINYBAR_TO_WEIBAR_COEF)) { throw predefined.VALUE_TOO_LOW; } @@ -47,9 +53,23 @@ export class Precheck { /** * @param transaction + * @param gasPrice + */ + async sendRawTransactionCheck(transaction: string, gasPrice: number) { + const parsedTx = Precheck.parseTxIfNeeded(transaction); + + this.gasLimit(parsedTx); + await this.nonce(parsedTx); + this.chainId(parsedTx); + this.value(parsedTx); + this.gasPrice(parsedTx, gasPrice); + await this.balance(parsedTx, EthImpl.ethSendRawTransaction); + } + + /** + * @param tx */ - async nonce(transaction: string) { - const tx = ethers.utils.parseTransaction(transaction); + async nonce(tx: Transaction) { const rsTx = await ethers.utils.resolveProperties({ gasPrice: tx.gasPrice, gasLimit: tx.gasLimit, @@ -73,67 +93,66 @@ export class Precheck { } } - - chainId(transaction: string) { - const tx = ethers.utils.parseTransaction(transaction); + /** + * @param tx + */ + chainId(tx: Transaction) { const txChainId = EthImpl.prepend0x(Number(tx.chainId).toString(16)); const passes = txChainId === this.chain; if (!passes) { - this.logger.trace('Failed chainId precheck for sendRawTransaction(transaction=%s, chainId=%s)', transaction, txChainId); + this.logger.trace('Failed chainId precheck for sendRawTransaction(transaction=%s, chainId=%s)', JSON.stringify(tx), txChainId); throw predefined.UNSUPPORTED_CHAIN_ID(txChainId, this.chain); } } - gasPrice(transaction: string, gasPrice: number) { - const tx = ethers.utils.parseTransaction(transaction); + /** + * @param tx + * @param gasPrice + */ + gasPrice(tx: Transaction, gasPrice: number) { const minGasPrice = ethers.ethers.BigNumber.from(gasPrice); const txGasPrice = tx.gasPrice || tx.maxFeePerGas!.add(tx.maxPriorityFeePerGas!); const passes = txGasPrice.gte(minGasPrice); if (!passes) { - this.logger.trace('Failed gas price precheck for sendRawTransaction(transaction=%s, gasPrice=%s, requiredGasPrice=%s)', transaction, txGasPrice, minGasPrice); + this.logger.trace('Failed gas price precheck for sendRawTransaction(transaction=%s, gasPrice=%s, requiredGasPrice=%s)', JSON.stringify(tx), txGasPrice, minGasPrice); + throw predefined.GAS_PRICE_TOO_LOW; } - - return { - passes, - error: predefined.GAS_PRICE_TOO_LOW - }; } - async balance(transaction: string, callerName: string) { + /** + * @param tx + * @param callerName + */ + async balance(tx: Transaction, callerName: string) { const result = { passes: false, error: predefined.INSUFFICIENT_ACCOUNT_BALANCE }; - - const tx = ethers.utils.parseTransaction(transaction); const txGas = tx.gasPrice || tx.maxFeePerGas!.add(tx.maxPriorityFeePerGas!); const txTotalValue = tx.value.add(txGas.mul(tx.gasLimit)); + let tinybars; try { const { account }: any = await this.mirrorNodeClient.getAccount(tx.from!); - const tinybars = await this.sdkClient.getAccountBalanceInTinyBar(account, callerName); + tinybars = await this.sdkClient.getAccountBalanceInTinyBar(account, callerName); result.passes = ethers.ethers.BigNumber.from(tinybars.toString()).mul(constants.TINYBAR_TO_WEIBAR_COEF).gte(txTotalValue); - - if (!result.passes) { - this.logger.trace('Failed balance precheck for sendRawTransaction(transaction=%s, totalValue=%s, accountTinyBarBalance=%s)', transaction, txTotalValue, tinybars); - } } catch (error: any) { - this.logger.trace('Error on balance precheck for sendRawTransaction(transaction=%s, totalValue=%s, error=%s)', transaction, txTotalValue, error.message); - - result.passes = false; - result.error = predefined.INTERNAL_ERROR; + this.logger.trace('Error on balance precheck for sendRawTransaction(transaction=%s, totalValue=%s, error=%s)', JSON.stringify(tx), txTotalValue, error.message); + throw predefined.INTERNAL_ERROR; } - return result; + if (!result.passes) { + this.logger.trace('Failed balance precheck for sendRawTransaction(transaction=%s, totalValue=%s, accountTinyBarBalance=%s)', JSON.stringify(tx), txTotalValue, tinybars); + throw predefined.INSUFFICIENT_ACCOUNT_BALANCE; + } } /** - * @param transaction + * @param tx */ - async gasLimit(transaction: string) { - const tx = ethers.utils.parseTransaction(transaction); + gasLimit(tx: Transaction) { const gasLimit = tx.gasLimit.toNumber(); const failBaseLog = 'Failed gasLimit precheck for sendRawTransaction(transaction=%s).'; @@ -141,10 +160,10 @@ export class Precheck { if (gasLimit > constants.BLOCK_GAS_LIMIT) { - this.logger.trace(`${failBaseLog} Gas Limit was too high: %s, block gas limit: %s`, transaction, gasLimit, constants.BLOCK_GAS_LIMIT); + this.logger.trace(`${failBaseLog} Gas Limit was too high: %s, block gas limit: %s`, JSON.stringify(tx), gasLimit, constants.BLOCK_GAS_LIMIT); throw predefined.GAS_LIMIT_TOO_HIGH; } else if (gasLimit < intrinsicGasCost) { - this.logger.trace(`${failBaseLog} Gas Limit was too low: %s, intrinsic gas cost: %s`, transaction, gasLimit, intrinsicGasCost); + this.logger.trace(`${failBaseLog} Gas Limit was too low: %s, intrinsic gas cost: %s`, JSON.stringify(tx), gasLimit, intrinsicGasCost); throw predefined.GAS_LIMIT_TOO_LOW; } } @@ -160,7 +179,7 @@ export class Precheck { let zeros = 0; - const dataBytes = Buffer.from(data, "hex"); + const dataBytes = Buffer.from(data, 'hex'); for (const c of dataBytes) { if (c == 0) { diff --git a/packages/relay/tests/lib/precheck.spec.ts b/packages/relay/tests/lib/precheck.spec.ts index 3af9e084d..ee6bf4195 100644 --- a/packages/relay/tests/lib/precheck.spec.ts +++ b/packages/relay/tests/lib/precheck.spec.ts @@ -33,16 +33,20 @@ import axios from "axios"; import MockAdapter from "axios-mock-adapter"; import { ethers } from "ethers"; import constants from '../../src/lib/constants'; -import { predefined } from './../../src/lib/errors'; const logger = pino(); describe('Precheck', async function() { const txWithMatchingChainId = '0x02f87482012a0485a7a358200085a7a3582000832dc6c09400000000000000000000000000000000000003f78502540be40080c001a006f4cd8e6f84b76a05a5c1542a08682c928108ef7163d9c1bf1f3b636b1cd1fba032097cbf2dda17a2dcc40f62c97964d9d930cdce2e8a9df9a8ba023cda28e4ad'; + const parsedTxWithMatchingChainId = ethers.utils.parseTransaction(txWithMatchingChainId); const txWithNonMatchingChainId = '0xf86a0385a7a3582000832dc6c09400000000000000000000000000000000000003f78502540be400801ca06750e92db52fa708e27f94f27e0cfb7f5800f9b657180bb2e94c1520cfb1fb6da01bec6045068b6db38b55017bb8b50166699384bc1791fd8331febab0cf629a2a'; + const parsedTxWithNonMatchingChainId = ethers.utils.parseTransaction(txWithNonMatchingChainId); const txWithValueMoreThanOneTinyBar = '0xf8628080809449858d4445908c12fcf70251d3f4682e8c9c381085174876e800801ba015ec73d3e329c7f5c0228be39bf30758f974d69468847dd507082c89ec453fe2a04124cc1dd6ac07417e7cdbe04cb99d698bddc6ce4d04054dd8978dec3493f3d2'; + const parsedTxWithValueMoreThanOneTinyBar = ethers.utils.parseTransaction(txWithValueMoreThanOneTinyBar); const txWithValueLessThanOneTinybar = '0xf8618080809449858d4445908c12fcf70251d3f4682e8c9c38108405f5e100801ba08249a7664c9290e6896711059d2ab75b10675b8b2ef7da41f4dd94c99f16f587a00110bc057ae0837da17a6f31f5123977f820921e333cb75fbe342583d278327d'; + const parsedTxWithValueLessThanOneTinybar = ethers.utils.parseTransaction(txWithValueLessThanOneTinybar); const txWithValueLessThanOneTinybarAndNotEmptyData = '0xf8638080809449858d4445908c12fcf70251d3f4682e8c9c3810830186a0831231231ba0d8d47f572b49be8da9866e1979ea8fb8060f885119aff9d457a77be088f03545a00c9c1266548930924f5f8c11854bcc369bda1449d203c86a15840759b61cdffe'; + const parsedTxWithValueLessThanOneTinybarAndNotEmptyData = ethers.utils.parseTransaction(txWithValueLessThanOneTinybarAndNotEmptyData); const oneTinyBar = ethers.utils.parseUnits('1', 10); const defaultGasPrice = 720_000_000_000; const defaultChainId = Number('0x12a'); @@ -80,7 +84,7 @@ describe('Precheck', async function() { it('should throw an exception if value is less than 1 tinybar', async function() { let hasError = false; try { - precheck.value(txWithValueLessThanOneTinybar); + precheck.value(parsedTxWithValueLessThanOneTinybar); } catch (e) { expect(e).to.exist; expect(e.code).to.eq(-32602); @@ -93,7 +97,7 @@ describe('Precheck', async function() { it('should pass if value is more than 1 tinybar', async function() { try { - precheck.value(txWithValueMoreThanOneTinyBar); + precheck.value(parsedTxWithValueMoreThanOneTinyBar); } catch (e) { expect(e).to.not.exist; } @@ -101,8 +105,8 @@ describe('Precheck', async function() { it('should pass if value is less than 1 tinybar and data is not empty', async function() { try { - precheck.value(txWithValueLessThanOneTinybarAndNotEmptyData); - } catch (e) { + precheck.value(parsedTxWithValueLessThanOneTinybarAndNotEmptyData); + } catch (e: any) { expect(e).to.not.exist; } }); @@ -111,16 +115,16 @@ describe('Precheck', async function() { describe('chainId', async function() { it('should pass for matching chainId', async function() { try { - precheck.chainId(txWithMatchingChainId); + precheck.chainId(parsedTxWithMatchingChainId); } - catch(e) { + catch(e: any) { expect(e).to.not.exist; } }); it('should not pass for non-matching chainId', async function() { try { - precheck.chainId(txWithNonMatchingChainId); + precheck.chainId(parsedTxWithNonMatchingChainId); expectedError(); } catch(e: any) { @@ -133,22 +137,18 @@ describe('Precheck', async function() { describe('gas price', async function() { it('should return true for gas price gt to required gas price', async function() { - const result = precheck.gasPrice(txWithMatchingChainId, 10); - expect(result).to.exist; - expect(result.error).to.exist; - expect(result.passes).to.eq(true); + const result = precheck.gasPrice(parsedTxWithMatchingChainId, 10); + expect(result).to.not.exist; }); it('should return true for gas price equal to required gas price', async function() { - const result = precheck.gasPrice(txWithMatchingChainId, defaultGasPrice); - expect(result).to.exist; - expect(result.error).to.exist; - expect(result.passes).to.eq(true); + const result = precheck.gasPrice(parsedTxWithMatchingChainId, defaultGasPrice); + expect(result).to.not.exist; }); it('should not pass for non-matching chainId', async function() { try { - precheck.chainId(txWithNonMatchingChainId); + precheck.chainId(parsedTxWithNonMatchingChainId); expectedError(); } catch(e: any) { @@ -158,12 +158,16 @@ describe('Precheck', async function() { } }); - it('should return false for gas price not enough', async function() { + it('should not pass for gas price not enough', async function() { const minGasPrice = 1000 * constants.TINYBAR_TO_WEIBAR_COEF; - const result = precheck.gasPrice(txWithMatchingChainId, minGasPrice); - expect(result).to.exist; - expect(result.error).to.exist; - expect(result.passes).to.eq(false); + try { + precheck.gasPrice(parsedTxWithMatchingChainId, minGasPrice); + expectedError(); + } catch(e: any) { + expect(e).to.exist; + expect(e.code).to.eq(-32009); + expect(e.message).to.eq('Gas price below configured minimum gas price'); + } }); }); @@ -182,9 +186,9 @@ describe('Precheck', async function() { gasLimit: gasLimit }; const signed = await signTransaction(tx); - + const parsedTx = ethers.utils.parseTransaction(signed); try { - await precheck.gasLimit(signed); + await precheck.gasLimit(parsedTx); expectedError(); } catch (e: any) { expect(e).to.exist; @@ -203,9 +207,10 @@ describe('Precheck', async function() { gasLimit: gasLimit }; const signed = await signTransaction(tx); + const parsedTx = ethers.utils.parseTransaction(signed); try { - await precheck.gasLimit(signed); + await precheck.gasLimit(parsedTx); } catch (e: any) { expect(e).to.not.exist; } @@ -223,100 +228,100 @@ describe('Precheck', async function() { }); describe('gas price', async function() { - it('should return true for gas price gt to required gas price', async function() { - const result = precheck.gasPrice(txWithMatchingChainId, 10); - expect(result).to.exist; - expect(result.error).to.exist; - expect(result.passes).to.eq(true); + it('should pass for gas price gt to required gas price', async function() { + const result = precheck.gasPrice(parsedTxWithMatchingChainId, 10); + expect(result).to.not.exist; }); - it('should return true for gas price equal to required gas price', async function() { - const result = precheck.gasPrice(txWithMatchingChainId, defaultGasPrice); - expect(result).to.exist; - expect(result.error).to.exist; - expect(result.passes).to.eq(true); + it('should pass for gas price equal to required gas price', async function() { + const result = precheck.gasPrice(parsedTxWithMatchingChainId, defaultGasPrice); + expect(result).to.not.exist; }); - it('should return false for gas price not enough', async function() { + it('should not pass for gas price not enough', async function() { const minGasPrice = 1000 * constants.TINYBAR_TO_WEIBAR_COEF; - const result = precheck.gasPrice(txWithMatchingChainId, minGasPrice); - expect(result).to.exist; - expect(result.error).to.exist; - expect(result.passes).to.eq(false); + try { + precheck.gasPrice(parsedTxWithMatchingChainId, minGasPrice); + expectedError(); + } catch (e: any) { + expect(e).to.exist; + expect(e.code).to.eq(-32009); + expect(e.message).to.eq('Gas price below configured minimum gas price'); + } }); }); describe('balance', async function() { // sending 2 hbars const transaction = '0x02f876820128078459682f0086018a4c7747008252089443cb701defe8fc6ed04d7bddf949618e3c575fe1881bc16d674ec8000080c001a0b8c604e08c15a7acc8c898a1bbcc41befcd0d120b64041d1086381c7fc2a5339a062eabec286592a7283c90ce90d97f9f8cf9f6c0cef4998022660e7573c046a46'; + const parsedTransaction = ethers.utils.parseTransaction(transaction); const mirrorAccountsPath = 'accounts/0xF8A44f9a4E4c452D25F5aE0F5d7970Ac9522a3C8'; const accountId = '0.1.2'; - it('should return false for 1 hbar', async function() { + it('should not pass for 1 hbar', async function() { mock.onGet(mirrorAccountsPath).reply(200, { account: accountId }); sdkInstance.getAccountBalanceInTinyBar.returns(Hbar.from(1, HbarUnit.Hbar).to(HbarUnit.Tinybar)); - const result = await precheck.balance(transaction, 'sendRawTransaction'); - expect(result).to.exist; - expect(result.error.message).to.eq(predefined.INSUFFICIENT_ACCOUNT_BALANCE.message); - expect(result.passes).to.eq(false); + try { + await precheck.balance(parsedTransaction, 'sendRawTransaction'); + expectedError(); + } catch(e: any) { + expect(e).to.exist; + expect(e.code).to.eq(-32000); + expect(e.message).to.eq('Insufficient funds for transfer'); + } }); - it('should return true for 10 hbar', async function() { + it('should pass for 10 hbar', async function() { mock.onGet(mirrorAccountsPath).reply(200, { account: accountId }); sdkInstance.getAccountBalanceInTinyBar.returns(Hbar.from(10, HbarUnit.Hbar).to(HbarUnit.Tinybar)); - const result = await precheck.balance(transaction, 'sendRawTransaction'); - expect(result).to.exist; - expect(result.passes).to.eq(true); + const result = await precheck.balance(parsedTransaction, 'sendRawTransaction'); + expect(result).to.not.exist; }); - it('should return true for 100 hbar', async function() { + it('should pass for 100 hbar', async function() { mock.onGet(mirrorAccountsPath).reply(200, { account: accountId }); sdkInstance.getAccountBalanceInTinyBar.returns(Hbar.from(100, HbarUnit.Hbar).to(HbarUnit.Tinybar)); - const result = await precheck.balance(transaction, 'sendRawTransaction'); - expect(result).to.exist; - expect(result.passes).to.eq(true); + const result = await precheck.balance(parsedTransaction, 'sendRawTransaction'); + expect(result).to.not.exist; }); - it('should return true for 10000 hbar', async function() { + it('should pass for 10000 hbar', async function() { mock.onGet(mirrorAccountsPath).reply(200, { account: accountId }); sdkInstance.getAccountBalanceInTinyBar.returns(Hbar.from(10_000, HbarUnit.Hbar).to(HbarUnit.Tinybar)); - const result = await precheck.balance(transaction, 'sendRawTransaction'); - expect(result).to.exist; - expect(result.passes).to.eq(true); + const result = await precheck.balance(parsedTransaction, 'sendRawTransaction'); + expect(result).to.not.exist; }); - it('should return true for 100000 hbar', async function() { + it('should pass for 100000 hbar', async function() { mock.onGet(mirrorAccountsPath).reply(200, { account: accountId }); sdkInstance.getAccountBalanceInTinyBar.returns(Hbar.from(100_000, HbarUnit.Hbar).to(HbarUnit.Tinybar)); - const result = await precheck.balance(transaction, 'sendRawTransaction'); - expect(result).to.exist; - expect(result.passes).to.eq(true); + const result = await precheck.balance(parsedTransaction, 'sendRawTransaction'); + expect(result).to.not.exist; }); - it('should return true for 50_000_000_000 hbar', async function() { + it('should pass for 50_000_000_000 hbar', async function() { mock.onGet(mirrorAccountsPath).reply(200, { account: accountId }); sdkInstance.getAccountBalanceInTinyBar.returns(Hbar.from(50_000_000_000, HbarUnit.Hbar).to(HbarUnit.Tinybar)); - const result = await precheck.balance(transaction, 'sendRawTransaction'); - expect(result).to.exist; - expect(result.passes).to.eq(true); + const result = await precheck.balance(parsedTransaction, 'sendRawTransaction'); + expect(result).to.not.exist; }); }); });