diff --git a/src/execute/execute.errors.ts b/src/execute/execute.errors.ts index 69c2fc8..b14cfa6 100644 --- a/src/execute/execute.errors.ts +++ b/src/execute/execute.errors.ts @@ -1,20 +1,39 @@ -export enum PROVIDER_ERRORS { - INVALID_ENDPOINT = 'Provider // Invalid endpoint!', +export enum QUEUE_ERRORS { + JOB_NOT_FOUND = 'Queue // A job with the provided id could not be found!', + REDIS_NOT_AVAILABLE = 'Queue // Could not connect to Redis!', } -export enum CONTRACT_ERRORS { - INVALID_CONTRACT = 'Contract // Invalid contract!', +export enum ExecuteProcessorError { + PROVIDER_INVALID_ENDPOINT = 'PROVIDER_INVALID_ENDPOINT', + CONTRACT_INVALID_ADDRESS = 'CONTRACT_INVALID_ADDRESS', + CONTRACT_INVALID_NO_CODE = 'CONTRACT_INVALID_NO_CODE', + WALLET_INVALID_PRIVATE_KEY = 'WALLET_INVALID_PRIVATE_KEY', + CERTIFICATE_NOT_FOUND = 'CERTIFICATE_NOT_FOUND', + EXECUTE_TRANSACTION_FAILED_INIT = 'EXECUTE_TRANSACTION_FAILED_INIT', + EXECUTE_TRANSACTION_REVERT = 'EXECUTE_TRANSACTION_REVERT', } -export enum WALLET_ERRORS { - INVALID_PRIVATE_KEY = 'Wallet // Invalid private key!', +export enum ExecuteProcessorErrorMessage { + PROVIDER_INVALID_ENDPOINT = 'Invalid subnet endpoint', + CONTRACT_INVALID_ADDRESS = 'Invalid messaging contract address', + CONTRACT_INVALID_NO_CODE = 'Invalid messaging contract (no code at address)', + WALLET_INVALID_PRIVATE_KEY = 'Invalid private key', + CERTIFICATE_NOT_FOUND = 'A certificate with the provided receipt trie root could not be found', + EXECUTE_TRANSACTION_FAILED_INIT = 'The execute transaction could not be created', } -export enum QUEUE_ERRORS { - JOB_NOT_FOUND = 'Queue // A job with the provided id could not be found!', - REDIS_NOT_AVAILABLE = 'Queue // Could not connect to Redis!', +export class ExecuteError extends Error { + constructor(type: ExecuteProcessorError, message?: string) { + const _message = JSON.stringify({ + type, + message: message || ExecuteProcessorErrorMessage[type], + }) + super(_message) + this.name = 'ExecuteError' + } } -export enum JOB_ERRORS { - MISSING_CERTIFICATE = 'Job // Could not find the related certificate!', +export interface ExecuteTransactionError { + decoded?: boolean + data: string } diff --git a/src/execute/execute.processor.spec.ts b/src/execute/execute.processor.spec.ts index 3d71a93..56c9a54 100644 --- a/src/execute/execute.processor.spec.ts +++ b/src/execute/execute.processor.spec.ts @@ -41,10 +41,15 @@ const subnetMock = { endpointWs: 'ws://endpoint/ws' } const providerMock = Object.assign(new EventEmitter(), { getCode: jest.fn().mockResolvedValue('0x123'), }) -const walletMock = {} -const transactionMock = { wait: jest.fn(() => Promise.resolve({})) } +const transactionMock = {} +const transactionResponseMock = { wait: jest.fn().mockResolvedValue({}) } +const walletMock = { + sendTransaction: jest.fn().mockResolvedValue(transactionResponseMock), +} const contractMock = { - execute: jest.fn().mockResolvedValue(transactionMock), + execute: { + populateTransaction: jest.fn().mockResolvedValue(transactionMock), + }, networkSubnetId: jest.fn().mockResolvedValue(''), subnets: jest.fn().mockResolvedValue(subnetMock), receiptRootToCertId: jest.fn().mockResolvedValue(''), @@ -112,7 +117,7 @@ describe('ExecuteProcessor', () => { expect(validExecuteJob.progress).toHaveBeenCalledWith(50) - expect(contractMock.execute).toHaveBeenCalledWith( + expect(contractMock.execute.populateTransaction).toHaveBeenCalledWith( validExecuteJob.data.logIndexes, validExecuteJob.data.receiptTrieRoot, validExecuteJob.data.receiptTrieMerkleProof, @@ -120,6 +125,10 @@ describe('ExecuteProcessor', () => { gasLimit: 4_000_000, } ) + + expect(walletMock.sendTransaction).toHaveBeenCalledWith(transactionMock) + expect(transactionResponseMock.wait).toHaveBeenCalled() + expect(validExecuteJob.progress).toHaveBeenCalledWith(100) }) }) diff --git a/src/execute/execute.processor.ts b/src/execute/execute.processor.ts index 805b8b2..bc0a2fb 100644 --- a/src/execute/execute.processor.ts +++ b/src/execute/execute.processor.ts @@ -29,7 +29,9 @@ import * as SubnetRegistratorJSON from '@topos-protocol/topos-smart-contracts/ar import { Job } from 'bull' import { Contract, + ContractTransaction, getDefaultProvider, + Interface, InterfaceAbi, Provider, Wallet, @@ -37,7 +39,11 @@ import { import { getErrorMessage } from '../utils' import { ExecuteDto } from './execute.dto' -import { CONTRACT_ERRORS, JOB_ERRORS, PROVIDER_ERRORS } from './execute.errors' +import { + ExecuteError, + ExecuteProcessorError, + ExecuteTransactionError, +} from './execute.errors' import { TracingOptions } from './execute.service' const UNDEFINED_CERTIFICATE_ID = @@ -123,20 +129,26 @@ export class ExecutionProcessorV1 { await job.progress(50) - const tx = await messagingContract.execute( + const transaction = await this._createExecuteTransaction( + messagingContract, logIndexes, receiptTrieMerkleProof, - receiptTrieRoot, - { - gasLimit: 4_000_000, - } + receiptTrieRoot ) - span.addEvent('got execute tx', { - tx: JSON.stringify(tx), + + await this._catchToposMessagingExecuteTransactionError( + provider, + transaction + ) + + const transactionResponse = await wallet.sendTransaction(transaction) + + span.addEvent('got execute transaction response', { + transaction: JSON.stringify(transactionResponse), }) - const receipt = await tx.wait() - span.addEvent('got execute tx receipt', { + const receipt = await transactionResponse.wait() + span.addEvent('got execute transaction receipt', { receipt: JSON.stringify(receipt), }) @@ -151,7 +163,6 @@ export class ExecutionProcessorV1 { message, }) span.end() - this.logger.debug('sync error', error) await job.moveToFailed({ message }) } }) @@ -205,15 +216,21 @@ export class ExecutionProcessorV1 { provider.on('debug', (data) => { if (data.error) { clearTimeout(timeoutId) - reject(new Error(PROVIDER_ERRORS.INVALID_ENDPOINT)) + reject( + new ExecuteError(ExecuteProcessorError.PROVIDER_INVALID_ENDPOINT) + ) } }) }) } private _createWallet(provider: Provider) { - const privateKey = this.configService.getOrThrow('PRIVATE_KEY') - return new Wallet(privateKey, provider) + try { + const privateKey = this.configService.getOrThrow('PRIVATE_KEY') + return new Wallet(privateKey, provider) + } catch (error) { + throw new ExecuteError(ExecuteProcessorError.WALLET_INVALID_PRIVATE_KEY) + } } private async _getContract( @@ -226,7 +243,7 @@ export class ExecutionProcessorV1 { const code = await provider.getCode(contractAddress) if (code === '0x') { - throw new Error() + throw new ExecuteError(ExecuteProcessorError.CONTRACT_INVALID_NO_CODE) } return new Contract( @@ -235,7 +252,11 @@ export class ExecutionProcessorV1 { wallet || provider ) } catch (error) { - throw new Error(CONTRACT_ERRORS.INVALID_CONTRACT) + if (error instanceof ExecuteError) { + throw error + } + + throw new ExecuteError(ExecuteProcessorError.CONTRACT_INVALID_ADDRESS) } } @@ -256,12 +277,58 @@ export class ExecutionProcessorV1 { } if (certId == UNDEFINED_CERTIFICATE_ID) { - throw new Error(JOB_ERRORS.MISSING_CERTIFICATE) + throw new ExecuteError(ExecuteProcessorError.CERTIFICATE_NOT_FOUND) } return certId } + private _createExecuteTransaction( + messagingContract: ToposMessaging, + logIndexes: number[], + receiptTrieMerkleProof: string, + receiptTrieRoot: string + ) { + try { + return messagingContract.execute.populateTransaction( + logIndexes, + receiptTrieMerkleProof, + receiptTrieRoot, + { + gasLimit: 4_000_000, + } + ) + } catch (error) { + throw new ExecuteError( + ExecuteProcessorError.EXECUTE_TRANSACTION_FAILED_INIT + ) + } + } + + private async _catchToposMessagingExecuteTransactionError( + provider: Provider, + transaction: ContractTransaction + ) { + try { + await provider.call(transaction) + } catch (error) { + if (error.data) { + const iface = new Interface(ToposMessagingJSON.abi) + const decodedError = iface.parseError(error.data) + + const transactionError: ExecuteTransactionError = { + decoded: Boolean(decodedError), + data: decodedError?.name || error.data, + } + + throw new ExecuteError( + ExecuteProcessorError.EXECUTE_TRANSACTION_REVERT, + JSON.stringify(transactionError) + ) + } + } + } + @OnGlobalQueueError() onGlobalQueueError(error: Error) { this.logger.error(error) diff --git a/src/execute/execute.service.ts b/src/execute/execute.service.ts index cfb5c73..499008d 100644 --- a/src/execute/execute.service.ts +++ b/src/execute/execute.service.ts @@ -7,7 +7,7 @@ import { isHexString } from 'ethers' import { Observable } from 'rxjs' import { ExecuteDto } from './execute.dto' -import { QUEUE_ERRORS, WALLET_ERRORS } from './execute.errors' +import { ExecuteProcessorErrorMessage, QUEUE_ERRORS } from './execute.errors' import { getErrorMessage } from '../utils' export interface TracingOptions { @@ -33,8 +33,6 @@ export class ExecuteServiceV1 { // attached to a root trace, while the local tracing options can only be // used for the work of adding the job to the queue return this._tracer.startActiveSpan('execute', (span) => { - this.logger.debug(rootTracingOptions) - return this._addExecutionJob(executeDto, rootTracingOptions) .then(({ id, timestamp }) => { span.setStatus({ code: SpanStatusCode.OK }) @@ -114,24 +112,23 @@ export class ExecuteServiceV1 { progressListener ) span.setStatus({ code: SpanStatusCode.OK }) + span.end() subscriber.next({ data: { payload, type: 'completed' } }) subscriber.complete() }) .catch((error) => { - this.logger.debug(`Job failed!`) - this.logger.debug(error) - span.setStatus({ code: SpanStatusCode.ERROR, message: error }) - subscriber.error(error) - subscriber.complete() - }) - .finally(() => { + const message = getErrorMessage(error) + span.setStatus({ code: SpanStatusCode.ERROR, message }) span.end() + subscriber.error(message) + subscriber.complete() }) }) .catch((error) => { + const message = getErrorMessage(error) this.logger.debug(`Job not found!`) this.logger.debug(error) - span.setStatus({ code: SpanStatusCode.ERROR, message: error }) + span.setStatus({ code: SpanStatusCode.ERROR, message }) span.end() subscriber.error(error) subscriber.complete() @@ -152,7 +149,7 @@ export class ExecuteServiceV1 { const privateKey = this.configService.get('PRIVATE_KEY') if (!isHexString(privateKey, 32)) { - throw new Error(WALLET_ERRORS.INVALID_PRIVATE_KEY) + throw new Error(ExecuteProcessorErrorMessage.WALLET_INVALID_PRIVATE_KEY) } }