Skip to content
This repository has been archived by the owner on Oct 31, 2024. It is now read-only.

Commit

Permalink
feat: return explicit execute errors (#20)
Browse files Browse the repository at this point in the history
  • Loading branch information
sebastiendan authored Feb 9, 2024
1 parent 8f56304 commit e6fed9d
Show file tree
Hide file tree
Showing 4 changed files with 136 additions and 44 deletions.
41 changes: 30 additions & 11 deletions src/execute/execute.errors.ts
Original file line number Diff line number Diff line change
@@ -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
}
17 changes: 13 additions & 4 deletions src/execute/execute.processor.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(''),
Expand Down Expand Up @@ -112,14 +117,18 @@ 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,
{
gasLimit: 4_000_000,
}
)

expect(walletMock.sendTransaction).toHaveBeenCalledWith(transactionMock)
expect(transactionResponseMock.wait).toHaveBeenCalled()

expect(validExecuteJob.progress).toHaveBeenCalledWith(100)
})
})
Expand Down
101 changes: 84 additions & 17 deletions src/execute/execute.processor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,15 +29,21 @@ import * as SubnetRegistratorJSON from '@topos-protocol/topos-smart-contracts/ar
import { Job } from 'bull'
import {
Contract,
ContractTransaction,
getDefaultProvider,
Interface,
InterfaceAbi,
Provider,
Wallet,
} from 'ethers'

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 =
Expand Down Expand Up @@ -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),
})

Expand All @@ -151,7 +163,6 @@ export class ExecutionProcessorV1 {
message,
})
span.end()
this.logger.debug('sync error', error)
await job.moveToFailed({ message })
}
})
Expand Down Expand Up @@ -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(
Expand All @@ -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(
Expand All @@ -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)
}
}

Expand All @@ -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)
Expand Down
21 changes: 9 additions & 12 deletions src/execute/execute.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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 })
Expand Down Expand Up @@ -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()
Expand All @@ -152,7 +149,7 @@ export class ExecuteServiceV1 {
const privateKey = this.configService.get<string>('PRIVATE_KEY')

if (!isHexString(privateKey, 32)) {
throw new Error(WALLET_ERRORS.INVALID_PRIVATE_KEY)
throw new Error(ExecuteProcessorErrorMessage.WALLET_INVALID_PRIVATE_KEY)
}
}

Expand Down

0 comments on commit e6fed9d

Please sign in to comment.