Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: return explicit execute errors #20

Merged
merged 2 commits into from
Feb 9, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading