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

Implement an error handling strategy by throwing custom errors. Resolves #135 #173

Draft
wants to merge 13 commits into
base: main
Choose a base branch
from
Draft
11 changes: 7 additions & 4 deletions src/__tests__/unit/gateway-interactions.loader.test.ts
Original file line number Diff line number Diff line change
@@ -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';

Expand Down Expand Up @@ -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 () => {
Expand All @@ -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();
}
});
});
25 changes: 19 additions & 6 deletions src/contract/Contract.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -12,12 +14,23 @@ export type BenchmarkStats = { gatewayCommunication: number; stateEvaluation: nu

export type SigningFunction = (tx: Transaction) => Promise<void>;

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<BundleInteractionErrorDetail> {}

// export class ContractError extends Error {
// constructor(message) {
// super(message);
// this.name = 'ContractError';
// }
// }

interface BundlrResponse {
id: string;
Expand Down
44 changes: 32 additions & 12 deletions src/contract/HandlerBasedContract.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -280,15 +282,30 @@ export class HandlerBasedContract<State> implements Contract<State> {
}
): Promise<WriteInteractionResponse | null> {
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',
Expand All @@ -308,7 +325,10 @@ export class HandlerBasedContract<State> implements Contract<State> {
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 {
Expand Down Expand Up @@ -336,7 +356,7 @@ export class HandlerBasedContract<State> implements Contract<State> {
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);
Expand All @@ -355,7 +375,7 @@ export class HandlerBasedContract<State> implements Contract<State> {
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}`);
}
}
}
Expand Down
9 changes: 9 additions & 0 deletions src/core/modules/InteractionsLoader.ts
Original file line number Diff line number Diff line change
@@ -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<InteractionsLoaderErrorDetail> {}

export type GW_TYPE = 'arweave' | 'warp';

export interface GwTypeAware {
Expand Down
5 changes: 3 additions & 2 deletions src/core/modules/impl/WarpGatewayInteractionsLoader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 =
Expand Down Expand Up @@ -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()}`);

Expand Down
4 changes: 2 additions & 2 deletions src/core/modules/impl/handler/AbstractContractHandler.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -89,7 +89,7 @@ export abstract class AbstractContractHandler<State> implements HandlerApi<State
}
});
if (shouldAutoThrow) {
throw new ContractError(effectiveErrorMessage);
throw new Error(effectiveErrorMessage);
}

return result;
Expand Down
15 changes: 15 additions & 0 deletions src/utils/CustomError.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
/**
* A helper type to avoid having to type `{ type: "..." }` for every error detail types.
*/
export type Err<T extends string> = { type: T };

/**
* The custom error type that every error originating from the library should extend.
*/
export class CustomError<T extends { type: string }> extends Error {
constructor(public detail: T, message?: string, public originalError?: unknown) {
super(`${detail.type}${message ? `: ${message}` : ''}`);
this.name = 'CustomError';
Error.captureStackTrace(this, CustomError);
}
}