diff --git a/src/__tests__/unit/gateway-interactions.loader.test.ts b/src/__tests__/unit/gateway-interactions.loader.test.ts index 05410926..f22ab5fd 100644 --- a/src/__tests__/unit/gateway-interactions.loader.test.ts +++ b/src/__tests__/unit/gateway-interactions.loader.test.ts @@ -1,6 +1,7 @@ import Arweave from 'arweave'; import { LexicographicalInteractionsSorter } from '../../core/modules/impl/LexicographicalInteractionsSorter'; import { WarpGatewayInteractionsLoader } from '../../core/modules/impl/WarpGatewayInteractionsLoader'; +import { InteractionsLoaderError } from '../../core/modules/InteractionsLoader'; import { GQLNodeInterface } from '../../legacy/gqlResult'; import { LoggerFactory } from '../../logging/LoggerFactory'; @@ -140,8 +141,9 @@ describe('WarpGatewayInteractionsLoader -> load', () => { const loader = new WarpGatewayInteractionsLoader('http://baseUrl'); try { await loader.load(contractId, fromBlockHeight, toBlockHeight); - } catch (e) { - expect(e).toEqual(new Error('Unable to retrieve transactions. Warp gateway responded with status 504.')); + } catch (rawError) { + const error = rawError as InteractionsLoaderError; + expect(error.detail.type === 'BadGatewayResponse' && error.detail.status === 504).toBeTruthy(); } }); it('should throw an error when request fails', async () => { @@ -151,8 +153,9 @@ describe('WarpGatewayInteractionsLoader -> load', () => { const loader = new WarpGatewayInteractionsLoader('http://baseUrl'); try { await loader.load(contractId, fromBlockHeight, toBlockHeight); - } catch (e) { - expect(e).toEqual(new Error('Unable to retrieve transactions. Warp gateway responded with status 500.')); + } catch (rawError) { + const error = rawError as InteractionsLoaderError; + expect(error.detail.type === 'BadGatewayResponse' && error.detail.status === 500).toBeTruthy(); } }); }); diff --git a/src/contract/Contract.ts b/src/contract/Contract.ts index 4376c773..5bc279a1 100644 --- a/src/contract/Contract.ts +++ b/src/contract/Contract.ts @@ -1,3 +1,5 @@ +import { BadGatewayResponse } from '../core/modules/InteractionsLoader'; +import { CustomError, Err } from '../utils/CustomError'; import Transaction from 'arweave/node/lib/transaction'; import { SortKeyCacheResult } from '../cache/SortKeyCache'; import { ContractCallRecord } from '../core/ContractCallRecord'; @@ -12,12 +14,23 @@ export type BenchmarkStats = { gatewayCommunication: number; stateEvaluation: nu export type SigningFunction = (tx: Transaction) => Promise; -export class ContractError extends Error { - constructor(message) { - super(message); - this.name = 'ContractError'; - } -} +// Make these two error cases individual as they could be used in different places +export type NoWalletConnected = Err<'NoWalletConnected'>; +export type InvalidInteraction = Err<'InvalidInteraction'>; + +export type BundleInteractionErrorDetail = + | NoWalletConnected + | InvalidInteraction + | BadGatewayResponse + | Err<'CannotBundle'>; +export class BundleInteractionError extends CustomError {} + +// export class ContractError extends Error { +// constructor(message) { +// super(message); +// this.name = 'ContractError'; +// } +// } interface BundlrResponse { id: string; diff --git a/src/contract/HandlerBasedContract.ts b/src/contract/HandlerBasedContract.ts index 3598ffaa..6d9ee9cb 100644 --- a/src/contract/HandlerBasedContract.ts +++ b/src/contract/HandlerBasedContract.ts @@ -30,12 +30,14 @@ import { CurrentTx, WriteInteractionOptions, WriteInteractionResponse, - InnerCallData + InnerCallData, + BundleInteractionError } from './Contract'; import { Tags, ArTransfer, emptyTransfer, ArWallet } from './deploy/CreateContract'; import { SourceData, SourceImpl } from './deploy/impl/SourceImpl'; import { InnerWritesEvaluator } from './InnerWritesEvaluator'; import { generateMockVrf } from '../utils/vrf'; +import { InteractionsLoaderError } from '../core/modules/InteractionsLoader'; /** * An implementation of {@link Contract} that is backwards compatible with current style @@ -280,15 +282,30 @@ export class HandlerBasedContract implements Contract { } ): Promise { this.logger.info('Bundle interaction input', input); + if (!this.signer) { + throw new BundleInteractionError( + { type: 'NoWalletConnected' }, + "Wallet not connected. Use 'connect' method first." + ); + } - const interactionTx = await this.createInteraction( - input, - options.tags, - emptyTransfer, - options.strict, - true, - options.vrf - ); + let interactionTx: Transaction; + try { + interactionTx = await this.createInteraction( + input, + options.tags, + emptyTransfer, + options.strict, + true, + options.vrf + ); + } catch (e) { + if (e instanceof InteractionsLoaderError) { + throw new BundleInteractionError(e.detail, `${e}`, e); + } else { + throw new BundleInteractionError({ type: 'InvalidInteraction' }, `${e}`, e); + } + } const response = await fetch(`${this._evaluationOptions.bundlerUrl}gateway/sequencer/register`, { method: 'POST', @@ -308,7 +325,10 @@ export class HandlerBasedContract implements Contract { if (error.body?.message) { this.logger.error(error.body.message); } - throw new Error(`Unable to bundle interaction: ${JSON.stringify(error)}`); + throw new BundleInteractionError( + { type: 'CannotBundle' }, + `Unable to bundle interaction: ${JSON.stringify(error)}` + ); }); return { @@ -336,7 +356,7 @@ export class HandlerBasedContract implements Contract { const handlerResult = await this.callContract(input, undefined, undefined, tags, transfer, strict, vrf); if (strict && handlerResult.type !== 'ok') { - throw Error(`Cannot create interaction: ${handlerResult.errorMessage}`); + throw new Error(`Cannot create interaction: ${handlerResult.errorMessage}`); } const callStack: ContractCallRecord = this.getCallStack(); const innerWrites = this._innerWritesEvaluator.eval(callStack); @@ -355,7 +375,7 @@ export class HandlerBasedContract implements Contract { if (strict) { const handlerResult = await this.callContract(input, undefined, undefined, tags, transfer, strict, vrf); if (handlerResult.type !== 'ok') { - throw Error(`Cannot create interaction: ${handlerResult.errorMessage}`); + throw new Error(`Cannot create interaction: ${handlerResult.errorMessage}`); } } } diff --git a/src/core/modules/InteractionsLoader.ts b/src/core/modules/InteractionsLoader.ts index 3bbc7c20..b058619b 100644 --- a/src/core/modules/InteractionsLoader.ts +++ b/src/core/modules/InteractionsLoader.ts @@ -1,6 +1,15 @@ +import { CustomError, Err } from '../../utils/CustomError'; import { GQLNodeInterface } from '../../legacy/gqlResult'; import { EvaluationOptions } from './StateEvaluator'; +// Make this error case individual as it is also used in `src/contract/Contract.ts`. +export type BadGatewayResponse = Err<'BadGatewayResponse'> & { status: number }; + +// InteractionsLoaderErrorDetail is effectively only an alias to BadGatewayResponse but it could +// also include other kinds of errors in the future. +export type InteractionsLoaderErrorDetail = BadGatewayResponse; +export class InteractionsLoaderError extends CustomError {} + export type GW_TYPE = 'arweave' | 'warp'; export interface GwTypeAware { diff --git a/src/core/modules/impl/WarpGatewayInteractionsLoader.ts b/src/core/modules/impl/WarpGatewayInteractionsLoader.ts index aefffeb9..a59a716f 100644 --- a/src/core/modules/impl/WarpGatewayInteractionsLoader.ts +++ b/src/core/modules/impl/WarpGatewayInteractionsLoader.ts @@ -3,7 +3,7 @@ import { Benchmark } from '../../../logging/Benchmark'; import { LoggerFactory } from '../../../logging/LoggerFactory'; import 'redstone-isomorphic'; import { stripTrailingSlash } from '../../../utils/utils'; -import { GW_TYPE, InteractionsLoader } from '../InteractionsLoader'; +import { GW_TYPE, InteractionsLoader, InteractionsLoaderError } from '../InteractionsLoader'; import { EvaluationOptions } from '../StateEvaluator'; export type ConfirmationStatus = @@ -91,7 +91,8 @@ export class WarpGatewayInteractionsLoader implements InteractionsLoader { if (error.body?.message) { this.logger.error(error.body.message); } - throw new Error(`Unable to retrieve transactions. Warp gateway responded with status ${error.status}.`); + const errorMessage = `Unable to retrieve transactions. Redstone gateway responded with status ${error.status}.`; + throw new InteractionsLoaderError({ type: 'BadGatewayResponse', status: error.status }, errorMessage, error); }); this.logger.debug(`Loading interactions: page ${page} loaded in ${benchmarkRequestTime.elapsed()}`); diff --git a/src/core/modules/impl/handler/AbstractContractHandler.ts b/src/core/modules/impl/handler/AbstractContractHandler.ts index 0c4a82ff..e95b6c32 100644 --- a/src/core/modules/impl/handler/AbstractContractHandler.ts +++ b/src/core/modules/impl/handler/AbstractContractHandler.ts @@ -1,4 +1,4 @@ -import { ContractError, CurrentTx } from '../../../../contract/Contract'; +import { CurrentTx } from '../../../../contract/Contract'; import { ContractDefinition } from '../../../../core/ContractDefinition'; import { ExecutionContext } from '../../../../core/ExecutionContext'; import { EvalStateResult } from '../../../../core/modules/StateEvaluator'; @@ -89,7 +89,7 @@ export abstract class AbstractContractHandler implements HandlerApi = { type: T }; + +/** + * The custom error type that every error originating from the library should extend. + */ +export class CustomError extends Error { + constructor(public detail: T, message?: string, public originalError?: unknown) { + super(`${detail.type}${message ? `: ${message}` : ''}`); + this.name = 'CustomError'; + Error.captureStackTrace(this, CustomError); + } +}