diff --git a/core/src/errors.ts b/core/src/errors.ts index b942e9db3..e03b349ba 100644 --- a/core/src/errors.ts +++ b/core/src/errors.ts @@ -2,6 +2,7 @@ export class IcnError extends Error { constructor(public readonly code: IcnErrorCode, message?: string, public readonly value?) { super(message) this.name = IcnErrorCode[code] + this.value = value Object.setPrototypeOf(this, new.target.prototype) } } @@ -22,5 +23,9 @@ export enum IcnErrorCode { InvalidPriceFeed, InvalidPriceFeedFormat, MissingKeyValuePair, - UnexpectedQueryOutput + UnexpectedQueryOutput, + TxInvalidAddress, + TxProcessingResponseError, + TxCannotEstimateGasError, + ProviderNetworkError } diff --git a/core/src/reporter/aggregator.ts b/core/src/reporter/aggregator.ts index 6a37af81a..d705e51a3 100644 --- a/core/src/reporter/aggregator.ts +++ b/core/src/reporter/aggregator.ts @@ -2,24 +2,21 @@ import { Worker } from 'bullmq' import { ethers } from 'ethers' import { Logger } from 'pino' import { Aggregator__factory } from '@bisonai-cic/icn-contracts' -import { sendTransaction, buildWallet } from './utils' +import { loadWalletParameters, sendTransaction, buildWallet } from './utils' import { REPORTER_AGGREGATOR_QUEUE_NAME, BULLMQ_CONNECTION } from '../settings' import { IAggregatorWorkerReporter } from '../types' const FILE_NAME = import.meta.url -export async function aggregatorReporter(_logger: Logger) { - _logger.debug({ name: 'aggregatorReporter', file: FILE_NAME }) +export async function reporter(_logger: Logger) { + _logger.debug({ name: 'reporter', file: FILE_NAME }) - const wallet = buildWallet(_logger) - new Worker( - REPORTER_AGGREGATOR_QUEUE_NAME, - await aggregatorJob(wallet, _logger), - BULLMQ_CONNECTION - ) + const { privateKey, providerUrl } = loadWalletParameters() + const wallet = buildWallet({ privateKey, providerUrl }) + new Worker(REPORTER_AGGREGATOR_QUEUE_NAME, await job(wallet, _logger), BULLMQ_CONNECTION) } -function aggregatorJob(wallet, _logger: Logger) { +function job(wallet, _logger: Logger) { const logger = _logger.child({ name: 'aggregatorJob', file: FILE_NAME }) const iface = new ethers.utils.Interface(Aggregator__factory.abi) diff --git a/core/src/reporter/main.ts b/core/src/reporter/main.ts index dde0412e3..153782ecc 100644 --- a/core/src/reporter/main.ts +++ b/core/src/reporter/main.ts @@ -1,12 +1,13 @@ import { parseArgs } from 'node:util' import { buildLogger } from '../logger' -import { aggregatorReporter } from './aggregator' -import { vrfReporter } from './vrf' +import { reporter as aggregatorReporter } from './aggregator' +import { reporter as vrfReporter } from './vrf' import { reporter as requestResponseReporter } from './request-response' import { launchHealthCheck } from '../health-check' import { hookConsoleError } from '../utils' +import { IReporters } from './types' -const REPORTERS = { +const REPORTERS: IReporters = { AGGREGATOR: aggregatorReporter, VRF: vrfReporter, REQUEST_RESPONSE: requestResponseReporter @@ -21,7 +22,7 @@ async function main() { launchHealthCheck() } -function loadArgs() { +function loadArgs(): string { const { values: { reporter } } = parseArgs({ diff --git a/core/src/reporter/request-response.ts b/core/src/reporter/request-response.ts index ba77bd670..1c383b36f 100644 --- a/core/src/reporter/request-response.ts +++ b/core/src/reporter/request-response.ts @@ -2,7 +2,7 @@ import { Worker } from 'bullmq' import { ethers } from 'ethers' import { Logger } from 'pino' import { RequestResponseCoordinator__factory } from '@bisonai-cic/icn-contracts' -import { sendTransaction, buildWallet } from './utils' +import { sendTransaction, loadWalletParameters, buildWallet } from './utils' import { REPORTER_REQUEST_RESPONSE_QUEUE_NAME, BULLMQ_CONNECTION } from '../settings' import { IRequestResponseWorkerReporter, RequestCommitmentRequestResponse } from '../types' @@ -10,7 +10,8 @@ const FILE_NAME = import.meta.url export async function reporter(_logger: Logger) { _logger.debug({ name: 'reporter', file: FILE_NAME }) - const wallet = buildWallet(_logger) + const { privateKey, providerUrl } = loadWalletParameters() + const wallet = buildWallet({ privateKey, providerUrl }) new Worker(REPORTER_REQUEST_RESPONSE_QUEUE_NAME, await job(wallet, _logger), BULLMQ_CONNECTION) } diff --git a/core/src/reporter/types.ts b/core/src/reporter/types.ts new file mode 100644 index 000000000..a7c86db20 --- /dev/null +++ b/core/src/reporter/types.ts @@ -0,0 +1,5 @@ +import { Logger } from 'pino' + +export interface IReporters { + [index: string]: (_logger: Logger) => Promise +} diff --git a/core/src/reporter/utils.ts b/core/src/reporter/utils.ts index a1bc064f5..b01cffb02 100644 --- a/core/src/reporter/utils.ts +++ b/core/src/reporter/utils.ts @@ -6,18 +6,34 @@ import { add0x } from '../utils' const FILE_NAME = import.meta.url -export function buildWallet(logger: Logger) { - try { - const { PRIVATE_KEY, PROVIDER } = checkParameters() - const provider = new ethers.providers.JsonRpcProvider(PROVIDER) - const wallet = new ethers.Wallet(PRIVATE_KEY, provider) - return wallet - } catch (e) { - logger.error(e) +export async function buildWallet({ + privateKey, + providerUrl, + testConnection +}: { + privateKey: string + providerUrl: string + testConnection?: boolean +}) { + const provider = new ethers.providers.JsonRpcProvider(providerUrl) + const wallet = new ethers.Wallet(privateKey, provider) + + if (testConnection) { + try { + await wallet.getTransactionCount() + } catch (e) { + if (e.code == 'NETWORK_ERROR') { + throw new IcnError(IcnErrorCode.ProviderNetworkError, 'ProviderNetworkError', e.reason) + } else { + throw e + } + } } + + return wallet } -function checkParameters() { +export function loadWalletParameters() { if (!PRIVATE_KEY_ENV) { throw new IcnError(IcnErrorCode.MissingMnemonic) } @@ -26,7 +42,7 @@ function checkParameters() { throw new IcnError(IcnErrorCode.MissingJsonRpcProvider) } - return { PRIVATE_KEY: PRIVATE_KEY_ENV, PROVIDER: PROVIDER_ENV } + return { privateKey: PRIVATE_KEY_ENV, providerUrl: PROVIDER_ENV } } export async function sendTransaction({ @@ -39,25 +55,47 @@ export async function sendTransaction({ }: { wallet to: string - payload: string + payload?: string gasLimit?: number | string - value?: number | string - _logger: Logger + value?: number | string | ethers.BigNumber + _logger?: Logger }) { - const logger = _logger.child({ name: 'sendTransaction', file: FILE_NAME }) + const logger = _logger?.child({ name: 'sendTransaction', file: FILE_NAME }) + + if (payload) { + payload = add0x(payload) + } const tx = { from: wallet.address, to: to, - data: add0x(payload), + data: payload || '0x00', value: value || '0x00' } if (gasLimit) { tx['gasLimit'] = gasLimit } - logger.debug(tx, 'tx') + logger?.debug(tx, 'tx') + + try { + const txReceipt = await (await wallet.sendTransaction(tx)).wait() + logger?.debug(txReceipt, 'txReceipt') + } catch (e) { + logger?.debug(e, 'e') - const txReceipt = await wallet.sendTransaction(tx) - logger.debug(txReceipt, 'txReceipt') + if (e.reason == 'invalid address') { + throw new IcnError(IcnErrorCode.TxInvalidAddress, 'TxInvalidAddress', e.value) + } else if (e.reason == 'processing response error') { + throw new IcnError( + IcnErrorCode.TxProcessingResponseError, + 'TxProcessingResponseError', + e.value + ) + } else if (e.code == 'UNPREDICTABLE_GAS_LIMIT') { + throw new IcnError(IcnErrorCode.TxCannotEstimateGasError, 'TxCannotEstimateGasError', e.value) + } else { + throw e + } + } } diff --git a/core/src/reporter/vrf.ts b/core/src/reporter/vrf.ts index 1c8592bdd..fb5f5900b 100644 --- a/core/src/reporter/vrf.ts +++ b/core/src/reporter/vrf.ts @@ -2,20 +2,21 @@ import { Worker } from 'bullmq' import { ethers } from 'ethers' import { Logger } from 'pino' import { VRFCoordinator__factory } from '@bisonai-cic/icn-contracts' -import { sendTransaction, buildWallet } from './utils' +import { loadWalletParameters, sendTransaction, buildWallet } from './utils' import { REPORTER_VRF_QUEUE_NAME, BULLMQ_CONNECTION } from '../settings' import { IVrfWorkerReporter, RequestCommitmentVRF, Proof } from '../types' const FILE_NAME = import.meta.url -export async function vrfReporter(_logger: Logger) { +export async function reporter(_logger: Logger) { _logger.debug({ name: 'vrfrReporter', file: FILE_NAME }) - const wallet = buildWallet(_logger) - new Worker(REPORTER_VRF_QUEUE_NAME, await vrfJob(wallet, _logger), BULLMQ_CONNECTION) + const { privateKey, providerUrl } = loadWalletParameters() + const wallet = buildWallet({ privateKey, providerUrl }) + new Worker(REPORTER_VRF_QUEUE_NAME, await job(wallet, _logger), BULLMQ_CONNECTION) } -function vrfJob(wallet, _logger: Logger) { - const logger = _logger.child({ name: 'vrfJob', file: FILE_NAME }) +function job(wallet, _logger: Logger) { + const logger = _logger.child({ name: 'job', file: FILE_NAME }) const iface = new ethers.utils.Interface(VRFCoordinator__factory.abi) const gasLimit = 3_000_000 // FIXME diff --git a/core/test/reporter.test.ts b/core/test/reporter.test.ts new file mode 100644 index 000000000..829cd9ee0 --- /dev/null +++ b/core/test/reporter.test.ts @@ -0,0 +1,61 @@ +import { describe, test, expect } from '@jest/globals' +import { buildWallet, sendTransaction } from '../src/reporter/utils' +import { ethers } from 'ethers' +import { IcnErrorCode } from '../src/errors' + +// The following tests have to be run with hardhat network launched. +// If the hardhat cannot be detected tests are skipped. +// TODO Include hardhat network launch to Github Actions pipeline. +describe('Reporter', function () { + const PROVIDER_URL = 'http://127.0.0.1:8545' + const PRIVATE_KEY = '0x5de4111afa1a4b94908f83103eb1f1706367c2e68ca870fc3fb9a804cdab365a' // hardhat account 2 + + test('Send payload to invalid address', async function () { + try { + const wallet = await buildWallet({ + privateKey: PRIVATE_KEY, + providerUrl: PROVIDER_URL, + testConnection: true + }) + + wallet.getBalance() + const to = '0x000000000000000000000000000000000000000' // wrong address + const payload = '0x' + + expect(async () => { + await sendTransaction({ wallet, to, payload }) + }).rejects.toThrow('TxInvalidAddress') + } catch (e) { + if (e.code == IcnErrorCode.ProviderNetworkError) { + return 0 + } else { + throw e + } + } + }) + + test('Send value without insufficient balance', async function () { + try { + const privateKey = '0xa5061ebc3567c2d3422807986c1c27425455fa62f4d9286c66d07a9afc6d9869' // account with 0 balance + + const wallet = await buildWallet({ + privateKey, + providerUrl: PROVIDER_URL, + testConnection: true + }) + + const to = '0x976EA74026E726554dB657fA54763abd0C3a0aa9' // hardhat account 6 + const value = ethers.utils.parseUnits('1') + + expect(async () => { + await sendTransaction({ wallet, to, value }) + }).rejects.toThrow('TxProcessingResponseError') + } catch (e) { + if (e.code == IcnErrorCode.ProviderNetworkError) { + return 0 + } else { + throw e + } + } + }) +})