From 316e4b6cca4c0e76e11aaca6da7fec6add8d25fa Mon Sep 17 00:00:00 2001 From: Eyal Chojnowski Date: Wed, 4 May 2022 20:32:55 +0200 Subject: [PATCH 1/8] feat: wip error handling for Contract --- src/contract/Contract.ts | 15 ++++ src/contract/HandlerBasedContract.ts | 112 +++++++++++++++++---------- src/utils/utils.ts | 10 +++ 3 files changed, 95 insertions(+), 42 deletions(-) diff --git a/src/contract/Contract.ts b/src/contract/Contract.ts index fb5cb3f4..d078f331 100644 --- a/src/contract/Contract.ts +++ b/src/contract/Contract.ts @@ -2,6 +2,7 @@ import { ArTransfer, ArWallet, ContractCallStack, + CustomError, EvalStateResult, EvaluationOptions, GQLNodeInterface, @@ -16,6 +17,20 @@ export type BenchmarkStats = { gatewayCommunication: number; stateEvaluation: nu export type SigningFunction = (tx: Transaction) => Promise; +export type ContractErrorKind = 'NoWalletConnected'; + +export type CreateInteractionErrorKind = 'InvalidInteraction' | 'UnknownError'; +export class CreateInteractionError extends CustomError {} + +export type BundleInteractionErrorKind = + | ContractErrorKind + | 'UnknownError' + | 'TxCreationFailed' + | 'InvalidInteraction' + | 'UnrecognizedGatewayStatus' + | 'CannotBundle'; +export class BundleInteractionError extends CustomError {} + /** * A base interface to be implemented by SmartWeave Contracts clients * - contains "low-level" methods that allow to interact with any contract diff --git a/src/contract/HandlerBasedContract.ts b/src/contract/HandlerBasedContract.ts index 7a50594d..4411a0b4 100644 --- a/src/contract/HandlerBasedContract.ts +++ b/src/contract/HandlerBasedContract.ts @@ -4,6 +4,7 @@ import { ArweaveWrapper, Benchmark, BenchmarkStats, + BundleInteractionError, Contract, ContractCallStack, ContractInteraction, @@ -29,7 +30,8 @@ import { SmartWeave, SmartWeaveTags, SourceType, - Tags + Tags, + CreateInteractionError } from '@smartweave'; import { TransactionStatusResponse } from 'arweave/node/transactions'; import { NetworkInfoInterface } from 'arweave/node/network'; @@ -229,9 +231,21 @@ export class HandlerBasedContract implements Contract { async bundleInteraction(input: Input, tags: Tags = [], strict = false): Promise { this.logger.info('Bundle interaction input', input); if (!this.signer) { - throw new Error("Wallet not connected. Use 'connect' method first."); + throw new BundleInteractionError('NoWalletConnected', "Wallet not connected. Use 'connect' method first."); + } + + let interactionTx; + try { + interactionTx = await this.createInteraction(input, tags, emptyTransfer, strict); + } catch (e) { + if (e instanceof CreateInteractionError) { + if (e.kind === 'InvalidInteraction') { + throw new BundleInteractionError('InvalidInteraction'); + } else { + throw new BundleInteractionError('UnknownError', undefined, e); + } + } } - const interactionTx = await this.createInteraction(input, tags, emptyTransfer, strict); const response = await fetch(`${this._evaluationOptions.bundlerAddress}gateway/sequencer/register`, { method: 'POST', @@ -251,7 +265,7 @@ 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('CannotBundle', `Unable to bundle interaction: ${JSON.stringify(error)}`); }); return { @@ -266,49 +280,63 @@ export class HandlerBasedContract implements Contract { transfer: ArTransfer, strict: boolean ) { - if (this._evaluationOptions.internalWrites) { - // Call contract and verify if there are any internal writes: - // 1. Evaluate current contract state - // 2. Apply input as "dry-run" transaction - // 3. Verify the callStack and search for any "internalWrites" transactions - // 4. For each found "internalWrite" transaction - generate additional tag: - // {name: 'InternalWrite', value: callingContractTxId} - const handlerResult = await this.callContract(input, undefined, undefined, tags, transfer); - if (strict && handlerResult.type !== 'ok') { - throw Error(`Cannot create interaction: ${handlerResult.errorMessage}`); - } - const callStack: ContractCallStack = this.getCallStack(); - const innerWrites = this._innerWritesEvaluator.eval(callStack); - this.logger.debug('Input', input); - this.logger.debug('Callstack', callStack.print()); - - innerWrites.forEach((contractTxId) => { - tags.push({ - name: SmartWeaveTags.INTERACT_WRITE, - value: contractTxId + try { + if (this._evaluationOptions.internalWrites) { + // Call contract and verify if there are any internal writes: + // 1. Evaluate current contract state + // 2. Apply input as "dry-run" transaction + // 3. Verify the callStack and search for any "internalWrites" transactions + // 4. For each found "internalWrite" transaction - generate additional tag: + // {name: 'InternalWrite', value: callingContractTxId} + const handlerResult = await this.callContract(input, undefined, undefined, tags, transfer); + if (strict && handlerResult.type !== 'ok') { + throw new CreateInteractionError( + 'InvalidInteraction', + `Cannot create interaction: ${handlerResult.errorMessage}` + ); + } + const callStack: ContractCallStack = this.getCallStack(); + const innerWrites = this._innerWritesEvaluator.eval(callStack); + this.logger.debug('Input', input); + this.logger.debug('Callstack', callStack.print()); + + innerWrites.forEach((contractTxId) => { + tags.push({ + name: SmartWeaveTags.INTERACT_WRITE, + value: contractTxId + }); }); - }); - this.logger.debug('Tags with inner calls', tags); - } else { - if (strict) { - const handlerResult = await this.callContract(input, undefined, undefined, tags, transfer); - if (handlerResult.type !== 'ok') { - throw Error(`Cannot create interaction: ${handlerResult.errorMessage}`); + this.logger.debug('Tags with inner calls', tags); + } else { + if (strict) { + const handlerResult = await this.callContract(input, undefined, undefined, tags, transfer); + if (handlerResult.type !== 'ok') { + throw new CreateInteractionError( + 'InvalidInteraction', + `Cannot create interaction: ${handlerResult.errorMessage}` + ); + } } } - } - const interactionTx = await createTx( - this.smartweave.arweave, - this.signer, - this._contractTxId, - input, - tags, - transfer.target, - transfer.winstonQty - ); - return interactionTx; + const interactionTx = await createTx( + this.smartweave.arweave, + this.signer, + this._contractTxId, + input, + tags, + transfer.target, + transfer.winstonQty + ); + return interactionTx; + } catch (e) { + if (e instanceof CreateInteractionError) { + throw e; + } else { + throw new CreateInteractionError('UnknownError', e, e); + } + } } txId(): string { diff --git a/src/utils/utils.ts b/src/utils/utils.ts index a8829bf7..5e53a2d5 100644 --- a/src/utils/utils.ts +++ b/src/utils/utils.ts @@ -55,3 +55,13 @@ export function timeout(s: number): { timeoutId: number; timeoutPromise: Promise export function stripTrailingSlash(str: string) { return str.endsWith('/') ? str.slice(0, -1) : str; } + +export class CustomError extends Error { + constructor(public kind: T, message?: string, public originalError?: unknown) { + super(`${kind}${message ? `: ${message}` : ''}`); + this.name = 'CustomError'; + Error.captureStackTrace(this, CustomError); + } +} + +type Constructor = new (...args: any[]) => T; From 81e7931ee4595e786e6dc24448008ffc3c3d116b Mon Sep 17 00:00:00 2001 From: Eyal Chojnowski Date: Fri, 6 May 2022 17:32:25 +0200 Subject: [PATCH 2/8] feat: simplify errors in Contract, add InteractionLoaderError --- src/contract/Contract.ts | 6 +- src/contract/HandlerBasedContract.ts | 106 ++++++++---------- src/core/modules/InteractionsLoader.ts | 7 +- .../impl/RedstoneGatewayInteractionsLoader.ts | 12 +- 4 files changed, 65 insertions(+), 66 deletions(-) diff --git a/src/contract/Contract.ts b/src/contract/Contract.ts index d078f331..60b21595 100644 --- a/src/contract/Contract.ts +++ b/src/contract/Contract.ts @@ -19,12 +19,12 @@ export type SigningFunction = (tx: Transaction) => Promise; export type ContractErrorKind = 'NoWalletConnected'; -export type CreateInteractionErrorKind = 'InvalidInteraction' | 'UnknownError'; -export class CreateInteractionError extends CustomError {} +export type CreateInteractionErrorKind = 'InvalidInteraction'; export type BundleInteractionErrorKind = | ContractErrorKind - | 'UnknownError' + | CreateInteractionErrorKind + | 'BadGatewayResponse' | 'TxCreationFailed' | 'InvalidInteraction' | 'UnrecognizedGatewayStatus' diff --git a/src/contract/HandlerBasedContract.ts b/src/contract/HandlerBasedContract.ts index 2a2c8e6e..bf57d73c 100644 --- a/src/contract/HandlerBasedContract.ts +++ b/src/contract/HandlerBasedContract.ts @@ -23,6 +23,7 @@ import { InnerWritesEvaluator, InteractionCall, InteractionData, + InteractionLoaderError, InteractionResult, LoggerFactory, SigningFunction, @@ -30,8 +31,7 @@ import { SmartWeave, SmartWeaveTags, SourceType, - Tags, - CreateInteractionError + Tags } from '@smartweave'; import { TransactionStatusResponse } from 'arweave/node/transactions'; import { NetworkInfoInterface } from 'arweave/node/network'; @@ -234,16 +234,14 @@ export class HandlerBasedContract implements Contract { throw new BundleInteractionError('NoWalletConnected', "Wallet not connected. Use 'connect' method first."); } - let interactionTx; + let interactionTx: Transaction; try { interactionTx = await this.createInteraction(input, tags, emptyTransfer, strict); } catch (e) { - if (e instanceof CreateInteractionError) { - if (e.kind === 'InvalidInteraction') { - throw new BundleInteractionError('InvalidInteraction'); - } else { - throw new BundleInteractionError('UnknownError', undefined, e); - } + if (e instanceof InteractionLoaderError) { + throw new BundleInteractionError('BadGatewayResponse'); + } else { + throw new BundleInteractionError('InvalidInteraction'); } } @@ -280,63 +278,49 @@ export class HandlerBasedContract implements Contract { transfer: ArTransfer, strict: boolean ) { - try { - if (this._evaluationOptions.internalWrites) { - // Call contract and verify if there are any internal writes: - // 1. Evaluate current contract state - // 2. Apply input as "dry-run" transaction - // 3. Verify the callStack and search for any "internalWrites" transactions - // 4. For each found "internalWrite" transaction - generate additional tag: - // {name: 'InternalWrite', value: callingContractTxId} - const handlerResult = await this.callContract(input, undefined, undefined, tags, transfer); - if (strict && handlerResult.type !== 'ok') { - throw new CreateInteractionError( - 'InvalidInteraction', - `Cannot create interaction: ${handlerResult.errorMessage}` - ); - } - const callStack: ContractCallStack = this.getCallStack(); - const innerWrites = this._innerWritesEvaluator.eval(callStack); - this.logger.debug('Input', input); - this.logger.debug('Callstack', callStack.print()); - - innerWrites.forEach((contractTxId) => { - tags.push({ - name: SmartWeaveTags.INTERACT_WRITE, - value: contractTxId - }); + if (this._evaluationOptions.internalWrites) { + // Call contract and verify if there are any internal writes: + // 1. Evaluate current contract state + // 2. Apply input as "dry-run" transaction + // 3. Verify the callStack and search for any "internalWrites" transactions + // 4. For each found "internalWrite" transaction - generate additional tag: + // {name: 'InternalWrite', value: callingContractTxId} + const handlerResult = await this.callContract(input, undefined, undefined, tags, transfer); + if (strict && handlerResult.type !== 'ok') { + throw new Error(`Cannot create interaction: ${handlerResult.errorMessage}`); + } + const callStack: ContractCallStack = this.getCallStack(); + const innerWrites = this._innerWritesEvaluator.eval(callStack); + this.logger.debug('Input', input); + this.logger.debug('Callstack', callStack.print()); + + innerWrites.forEach((contractTxId) => { + tags.push({ + name: SmartWeaveTags.INTERACT_WRITE, + value: contractTxId }); + }); - this.logger.debug('Tags with inner calls', tags); - } else { - if (strict) { - const handlerResult = await this.callContract(input, undefined, undefined, tags, transfer); - if (handlerResult.type !== 'ok') { - throw new CreateInteractionError( - 'InvalidInteraction', - `Cannot create interaction: ${handlerResult.errorMessage}` - ); - } + this.logger.debug('Tags with inner calls', tags); + } else { + if (strict) { + const handlerResult = await this.callContract(input, undefined, undefined, tags, transfer); + if (handlerResult.type !== 'ok') { + throw new Error(`Cannot create interaction: ${handlerResult.errorMessage}`); } } - - const interactionTx = await createTx( - this.smartweave.arweave, - this.signer, - this._contractTxId, - input, - tags, - transfer.target, - transfer.winstonQty - ); - return interactionTx; - } catch (e) { - if (e instanceof CreateInteractionError) { - throw e; - } else { - throw new CreateInteractionError('UnknownError', e, e); - } } + + const interactionTx = await createTx( + this.smartweave.arweave, + this.signer, + this._contractTxId, + input, + tags, + transfer.target, + transfer.winstonQty + ); + return interactionTx; } txId(): string { diff --git a/src/core/modules/InteractionsLoader.ts b/src/core/modules/InteractionsLoader.ts index 9cc7c393..16fb620f 100644 --- a/src/core/modules/InteractionsLoader.ts +++ b/src/core/modules/InteractionsLoader.ts @@ -1,4 +1,9 @@ -import { EvaluationOptions, GQLEdgeInterface } from '@smartweave'; +import { CustomError, EvaluationOptions, GQLEdgeInterface } from '@smartweave'; + +// TODO: Update tests at `src/__tests__/unit/gateway-interactions.loader.test.ts:140 & 151` to use +// this instead of comparing with error's message. +export type InteractionLoaderErrorKind = 'BadGatewayResponse500' | 'BadGatewayResponse504' | 'BadGatewayResponse'; +export class InteractionLoaderError extends CustomError {} /** * Implementors of this interface add functionality of loading contract's interaction transactions. diff --git a/src/core/modules/impl/RedstoneGatewayInteractionsLoader.ts b/src/core/modules/impl/RedstoneGatewayInteractionsLoader.ts index 240cc103..d014d8b8 100644 --- a/src/core/modules/impl/RedstoneGatewayInteractionsLoader.ts +++ b/src/core/modules/impl/RedstoneGatewayInteractionsLoader.ts @@ -8,6 +8,7 @@ import { stripTrailingSlash } from '@smartweave'; import 'redstone-isomorphic'; +import { InteractionLoaderError } from '../InteractionsLoader'; interface Paging { total: string; @@ -123,7 +124,16 @@ export class RedstoneGatewayInteractionsLoader implements InteractionsLoader { if (error.body?.message) { this.logger.error(error.body.message); } - throw new Error(`Unable to retrieve transactions. Redstone gateway responded with status ${error.status}.`); + const errorMessage = `Unable to retrieve transactions. Redstone gateway responded with status ${error.status}.`; + switch (error.status) { + case 504: + throw new InteractionLoaderError('BadGatewayResponse500', errorMessage); + case 500: + throw new InteractionLoaderError('BadGatewayResponse504', errorMessage); + + default: + throw new InteractionLoaderError('BadGatewayResponse', errorMessage); + } }); totalPages = response.paging.pages; From 4cc14670572c9ea45fdf2856177c8bae20f4a6d0 Mon Sep 17 00:00:00 2001 From: Eyal Chojnowski Date: Fri, 6 May 2022 20:33:32 +0200 Subject: [PATCH 3/8] fix: remove unused errors in BundleInteractionErrorKind --- src/contract/Contract.ts | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/contract/Contract.ts b/src/contract/Contract.ts index 60b21595..55102b67 100644 --- a/src/contract/Contract.ts +++ b/src/contract/Contract.ts @@ -25,9 +25,6 @@ export type BundleInteractionErrorKind = | ContractErrorKind | CreateInteractionErrorKind | 'BadGatewayResponse' - | 'TxCreationFailed' - | 'InvalidInteraction' - | 'UnrecognizedGatewayStatus' | 'CannotBundle'; export class BundleInteractionError extends CustomError {} From 1010585f68be1c8ebf485b7848c00926c6588fe5 Mon Sep 17 00:00:00 2001 From: Eyal Chojnowski Date: Sun, 8 May 2022 12:31:57 +0200 Subject: [PATCH 4/8] fix: circular import for CustomError --- src/contract/Contract.ts | 2 +- src/contract/HandlerBasedContract.ts | 4 ++-- src/core/modules/InteractionsLoader.ts | 7 ++++--- .../modules/impl/RedstoneGatewayInteractionsLoader.ts | 10 +++++----- src/utils/CustomError.ts | 7 +++++++ src/utils/index.ts | 1 + src/utils/utils.ts | 10 ---------- 7 files changed, 20 insertions(+), 21 deletions(-) create mode 100644 src/utils/CustomError.ts diff --git a/src/contract/Contract.ts b/src/contract/Contract.ts index 55102b67..e1c31558 100644 --- a/src/contract/Contract.ts +++ b/src/contract/Contract.ts @@ -2,13 +2,13 @@ import { ArTransfer, ArWallet, ContractCallStack, - CustomError, EvalStateResult, EvaluationOptions, GQLNodeInterface, InteractionResult, Tags } from '@smartweave'; +import { CustomError } from '@smartweave/utils'; import { NetworkInfoInterface } from 'arweave/node/network'; import Transaction from 'arweave/node/lib/transaction'; diff --git a/src/contract/HandlerBasedContract.ts b/src/contract/HandlerBasedContract.ts index bf57d73c..0f72db82 100644 --- a/src/contract/HandlerBasedContract.ts +++ b/src/contract/HandlerBasedContract.ts @@ -23,7 +23,7 @@ import { InnerWritesEvaluator, InteractionCall, InteractionData, - InteractionLoaderError, + InteractionsLoaderError, InteractionResult, LoggerFactory, SigningFunction, @@ -238,7 +238,7 @@ export class HandlerBasedContract implements Contract { try { interactionTx = await this.createInteraction(input, tags, emptyTransfer, strict); } catch (e) { - if (e instanceof InteractionLoaderError) { + if (e instanceof InteractionsLoaderError) { throw new BundleInteractionError('BadGatewayResponse'); } else { throw new BundleInteractionError('InvalidInteraction'); diff --git a/src/core/modules/InteractionsLoader.ts b/src/core/modules/InteractionsLoader.ts index 16fb620f..aa790c9a 100644 --- a/src/core/modules/InteractionsLoader.ts +++ b/src/core/modules/InteractionsLoader.ts @@ -1,9 +1,10 @@ -import { CustomError, EvaluationOptions, GQLEdgeInterface } from '@smartweave'; +import { EvaluationOptions, GQLEdgeInterface } from '@smartweave'; +import { CustomError } from '@smartweave/utils'; // TODO: Update tests at `src/__tests__/unit/gateway-interactions.loader.test.ts:140 & 151` to use // this instead of comparing with error's message. -export type InteractionLoaderErrorKind = 'BadGatewayResponse500' | 'BadGatewayResponse504' | 'BadGatewayResponse'; -export class InteractionLoaderError extends CustomError {} +export type InteractionsLoaderErrorKind = 'BadGatewayResponse500' | 'BadGatewayResponse504' | 'BadGatewayResponse'; +export class InteractionsLoaderError extends CustomError {} /** * Implementors of this interface add functionality of loading contract's interaction transactions. diff --git a/src/core/modules/impl/RedstoneGatewayInteractionsLoader.ts b/src/core/modules/impl/RedstoneGatewayInteractionsLoader.ts index d014d8b8..76257b9c 100644 --- a/src/core/modules/impl/RedstoneGatewayInteractionsLoader.ts +++ b/src/core/modules/impl/RedstoneGatewayInteractionsLoader.ts @@ -5,10 +5,10 @@ import { GQLNodeInterface, InteractionsLoader, LoggerFactory, - stripTrailingSlash + stripTrailingSlash, + InteractionsLoaderError } from '@smartweave'; import 'redstone-isomorphic'; -import { InteractionLoaderError } from '../InteractionsLoader'; interface Paging { total: string; @@ -127,12 +127,12 @@ export class RedstoneGatewayInteractionsLoader implements InteractionsLoader { const errorMessage = `Unable to retrieve transactions. Redstone gateway responded with status ${error.status}.`; switch (error.status) { case 504: - throw new InteractionLoaderError('BadGatewayResponse500', errorMessage); + throw new InteractionsLoaderError('BadGatewayResponse504', errorMessage); case 500: - throw new InteractionLoaderError('BadGatewayResponse504', errorMessage); + throw new InteractionsLoaderError('BadGatewayResponse500', errorMessage); default: - throw new InteractionLoaderError('BadGatewayResponse', errorMessage); + throw new InteractionsLoaderError('BadGatewayResponse', errorMessage); } }); totalPages = response.paging.pages; diff --git a/src/utils/CustomError.ts b/src/utils/CustomError.ts new file mode 100644 index 00000000..ee255568 --- /dev/null +++ b/src/utils/CustomError.ts @@ -0,0 +1,7 @@ +export class CustomError extends Error { + constructor(public kind: T, message?: string, public originalError?: unknown) { + super(`${kind}${message ? `: ${message}` : ''}`); + this.name = 'CustomError'; + Error.captureStackTrace(this, CustomError); + } +} diff --git a/src/utils/index.ts b/src/utils/index.ts index d1e299cd..8dba6480 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -1,2 +1,3 @@ export * from './utils'; export * from './ArweaveWrapper'; +export * from './CustomError'; diff --git a/src/utils/utils.ts b/src/utils/utils.ts index 5e53a2d5..a8829bf7 100644 --- a/src/utils/utils.ts +++ b/src/utils/utils.ts @@ -55,13 +55,3 @@ export function timeout(s: number): { timeoutId: number; timeoutPromise: Promise export function stripTrailingSlash(str: string) { return str.endsWith('/') ? str.slice(0, -1) : str; } - -export class CustomError extends Error { - constructor(public kind: T, message?: string, public originalError?: unknown) { - super(`${kind}${message ? `: ${message}` : ''}`); - this.name = 'CustomError'; - Error.captureStackTrace(this, CustomError); - } -} - -type Constructor = new (...args: any[]) => T; From e22a19796d7c51af28f168a5aa508ba001308bf2 Mon Sep 17 00:00:00 2001 From: Eyal Chojnowski Date: Sun, 8 May 2022 12:33:27 +0200 Subject: [PATCH 5/8] test: update gateway interactions tests --- src/__tests__/unit/gateway-interactions.loader.test.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/__tests__/unit/gateway-interactions.loader.test.ts b/src/__tests__/unit/gateway-interactions.loader.test.ts index db89a598..912f839b 100644 --- a/src/__tests__/unit/gateway-interactions.loader.test.ts +++ b/src/__tests__/unit/gateway-interactions.loader.test.ts @@ -1,4 +1,4 @@ -import { LoggerFactory, RedstoneGatewayInteractionsLoader } from '@smartweave'; +import { LoggerFactory, RedstoneGatewayInteractionsLoader, InteractionsLoaderError } from '@smartweave'; import { GQLEdgeInterface } from '../../legacy/gqlResult'; const responseData = { @@ -137,7 +137,7 @@ describe('RedstoneGatewayInteractionsLoader -> load', () => { try { await loader.load(contractId, fromBlockHeight, toBlockHeight); } catch (e) { - expect(e).toEqual(new Error('Unable to retrieve transactions. Redstone gateway responded with status 504.')); + expect((e as InteractionsLoaderError).kind === 'BadGatewayResponse504').toBeTruthy(); } }); it('should throw an error when request fails', async () => { @@ -148,7 +148,8 @@ describe('RedstoneGatewayInteractionsLoader -> load', () => { try { await loader.load(contractId, fromBlockHeight, toBlockHeight); } catch (e) { - expect(e).toEqual(new Error('Unable to retrieve transactions. Redstone gateway responded with status 500.')); + console.log(e); + expect((e as InteractionsLoaderError).kind === 'BadGatewayResponse500').toBeTruthy(); } }); }); From 8fc8a6b2f1227e55e4fa8adee0725522e07200a7 Mon Sep 17 00:00:00 2001 From: Eyal Chojnowski Date: Sun, 8 May 2022 13:16:44 +0200 Subject: [PATCH 6/8] feat: pass original errors when throwing custom errors --- src/contract/HandlerBasedContract.ts | 4 ++-- src/core/modules/impl/RedstoneGatewayInteractionsLoader.ts | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/contract/HandlerBasedContract.ts b/src/contract/HandlerBasedContract.ts index 0f72db82..dc4fefc2 100644 --- a/src/contract/HandlerBasedContract.ts +++ b/src/contract/HandlerBasedContract.ts @@ -239,9 +239,9 @@ export class HandlerBasedContract implements Contract { interactionTx = await this.createInteraction(input, tags, emptyTransfer, strict); } catch (e) { if (e instanceof InteractionsLoaderError) { - throw new BundleInteractionError('BadGatewayResponse'); + throw new BundleInteractionError('BadGatewayResponse', `${e}`, e); } else { - throw new BundleInteractionError('InvalidInteraction'); + throw new BundleInteractionError('InvalidInteraction', `${e}`, e); } } diff --git a/src/core/modules/impl/RedstoneGatewayInteractionsLoader.ts b/src/core/modules/impl/RedstoneGatewayInteractionsLoader.ts index 76257b9c..ba784140 100644 --- a/src/core/modules/impl/RedstoneGatewayInteractionsLoader.ts +++ b/src/core/modules/impl/RedstoneGatewayInteractionsLoader.ts @@ -127,12 +127,12 @@ export class RedstoneGatewayInteractionsLoader implements InteractionsLoader { const errorMessage = `Unable to retrieve transactions. Redstone gateway responded with status ${error.status}.`; switch (error.status) { case 504: - throw new InteractionsLoaderError('BadGatewayResponse504', errorMessage); + throw new InteractionsLoaderError('BadGatewayResponse504', errorMessage, error); case 500: - throw new InteractionsLoaderError('BadGatewayResponse500', errorMessage); + throw new InteractionsLoaderError('BadGatewayResponse500', errorMessage, error); default: - throw new InteractionsLoaderError('BadGatewayResponse', errorMessage); + throw new InteractionsLoaderError('BadGatewayResponse', errorMessage, error); } }); totalPages = response.paging.pages; From 6f0b0a4ce22b3c209c7e2016a5c5667b853c511c Mon Sep 17 00:00:00 2001 From: Eyal Chojnowski Date: Mon, 13 Jun 2022 11:21:39 +0200 Subject: [PATCH 7/8] fix: update code with upstream --- src/contract/HandlerBasedContract.ts | 32 +++++++++++++--------------- 1 file changed, 15 insertions(+), 17 deletions(-) diff --git a/src/contract/HandlerBasedContract.ts b/src/contract/HandlerBasedContract.ts index 5c7ad0a5..ac5ebe65 100644 --- a/src/contract/HandlerBasedContract.ts +++ b/src/contract/HandlerBasedContract.ts @@ -246,9 +246,23 @@ export class HandlerBasedContract implements Contract { throw new BundleInteractionError('NoWalletConnected', "Wallet not connected. Use 'connect' method first."); } + options = { + tags: [], + strict: false, + vrf: false, + ...options + }; + let interactionTx: Transaction; try { - interactionTx = await this.createInteraction(input, tags, emptyTransfer, strict); + interactionTx = await this.createInteraction( + input, + options.tags, + emptyTransfer, + options.strict, + true, + options.vrf + ); } catch (e) { if (e instanceof InteractionsLoaderError) { throw new BundleInteractionError('BadGatewayResponse', `${e}`, e); @@ -257,22 +271,6 @@ export class HandlerBasedContract implements Contract { } } - options = { - tags: [], - strict: false, - vrf: false, - ...options - }; - - const interactionTx = await this.createInteraction( - input, - options.tags, - emptyTransfer, - options.strict, - true, - options.vrf - ); - const response = await fetch(`${this._evaluationOptions.bundlerUrl}gateway/sequencer/register`, { method: 'POST', body: JSON.stringify(interactionTx), From bd068a665483e1d00b2c639f5659cd7105aab3fe Mon Sep 17 00:00:00 2001 From: Eyal Chojnowski Date: Fri, 24 Jun 2022 17:02:02 +0200 Subject: [PATCH 8/8] feat: allow every error type to include more detail --- .../unit/gateway-interactions.loader.test.ts | 10 +++++---- src/contract/Contract.ts | 21 ++++++++++--------- src/contract/HandlerBasedContract.ts | 14 +++++++++---- src/core/modules/InteractionsLoader.ts | 13 +++++++----- .../impl/WarpGatewayInteractionsLoader.ts | 11 ++-------- src/utils/CustomError.ts | 14 ++++++++++--- 6 files changed, 48 insertions(+), 35 deletions(-) diff --git a/src/__tests__/unit/gateway-interactions.loader.test.ts b/src/__tests__/unit/gateway-interactions.loader.test.ts index 498c6993..fc31b390 100644 --- a/src/__tests__/unit/gateway-interactions.loader.test.ts +++ b/src/__tests__/unit/gateway-interactions.loader.test.ts @@ -136,8 +136,9 @@ describe('WarpGatewayInteractionsLoader -> load', () => { const loader = new WarpGatewayInteractionsLoader('http://baseUrl'); try { await loader.load(contractId, fromBlockHeight, toBlockHeight); - } catch (e) { - expect((e as InteractionsLoaderError).kind === 'BadGatewayResponse504').toBeTruthy(); + } 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 () => { @@ -147,8 +148,9 @@ describe('WarpGatewayInteractionsLoader -> load', () => { const loader = new WarpGatewayInteractionsLoader('http://baseUrl'); try { await loader.load(contractId, fromBlockHeight, toBlockHeight); - } catch (e) { - expect((e as InteractionsLoaderError).kind === 'BadGatewayResponse500').toBeTruthy(); + } 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 4f765526..8b086480 100644 --- a/src/contract/Contract.ts +++ b/src/contract/Contract.ts @@ -1,6 +1,7 @@ import { ArTransfer, ArWallet, + BadGatewayResponse, ContractCallStack, EvalStateResult, EvaluationOptions, @@ -8,7 +9,7 @@ import { InteractionResult, Tags } from '@warp'; -import { CustomError } from '@warp/utils'; +import { CustomError, Err } from '@warp/utils'; import { NetworkInfoInterface } from 'arweave/node/network'; import Transaction from 'arweave/node/lib/transaction'; import { Source } from './deploy/Source'; @@ -18,16 +19,16 @@ export type BenchmarkStats = { gatewayCommunication: number; stateEvaluation: nu export type SigningFunction = (tx: Transaction) => Promise; -export type ContractErrorKind = 'NoWalletConnected'; +// 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 CreateInteractionErrorKind = 'InvalidInteraction'; - -export type BundleInteractionErrorKind = - | ContractErrorKind - | CreateInteractionErrorKind - | 'BadGatewayResponse' - | 'CannotBundle'; -export class BundleInteractionError extends CustomError {} +export type BundleInteractionErrorDetail = + | NoWalletConnected + | InvalidInteraction + | BadGatewayResponse + | Err<'CannotBundle'>; +export class BundleInteractionError extends CustomError {} /** * Interface describing state for all Evolve-compatible contracts. diff --git a/src/contract/HandlerBasedContract.ts b/src/contract/HandlerBasedContract.ts index 9f48fb26..c7800e93 100644 --- a/src/contract/HandlerBasedContract.ts +++ b/src/contract/HandlerBasedContract.ts @@ -244,7 +244,10 @@ export class HandlerBasedContract implements Contract { ): Promise { this.logger.info('Bundle interaction input', input); if (!this.signer) { - throw new BundleInteractionError('NoWalletConnected', "Wallet not connected. Use 'connect' method first."); + throw new BundleInteractionError( + { type: 'NoWalletConnected' }, + "Wallet not connected. Use 'connect' method first." + ); } options = { @@ -266,9 +269,9 @@ export class HandlerBasedContract implements Contract { ); } catch (e) { if (e instanceof InteractionsLoaderError) { - throw new BundleInteractionError('BadGatewayResponse', `${e}`, e); + throw new BundleInteractionError(e.detail, `${e}`, e); } else { - throw new BundleInteractionError('InvalidInteraction', `${e}`, e); + throw new BundleInteractionError({ type: 'InvalidInteraction' }, `${e}`, e); } } @@ -290,7 +293,10 @@ export class HandlerBasedContract implements Contract { if (error.body?.message) { this.logger.error(error.body.message); } - throw new BundleInteractionError('CannotBundle', `Unable to bundle interaction: ${JSON.stringify(error)}`); + throw new BundleInteractionError( + { type: 'CannotBundle' }, + `Unable to bundle interaction: ${JSON.stringify(error)}` + ); }); return { diff --git a/src/core/modules/InteractionsLoader.ts b/src/core/modules/InteractionsLoader.ts index 8143c004..2b647f34 100644 --- a/src/core/modules/InteractionsLoader.ts +++ b/src/core/modules/InteractionsLoader.ts @@ -1,10 +1,13 @@ import { EvaluationOptions, GQLEdgeInterface } from '@warp'; -import { CustomError } from '@warp/utils'; +import { CustomError, Err } from '@warp/utils'; -// TODO: Update tests at `src/__tests__/unit/gateway-interactions.loader.test.ts:140 & 151` to use -// this instead of comparing with error's message. -export type InteractionsLoaderErrorKind = 'BadGatewayResponse500' | 'BadGatewayResponse504' | 'BadGatewayResponse'; -export class InteractionsLoaderError extends CustomError {} +// 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 {} /** * Implementors of this interface add functionality of loading contract's interaction transactions. diff --git a/src/core/modules/impl/WarpGatewayInteractionsLoader.ts b/src/core/modules/impl/WarpGatewayInteractionsLoader.ts index abc15989..48057d8e 100644 --- a/src/core/modules/impl/WarpGatewayInteractionsLoader.ts +++ b/src/core/modules/impl/WarpGatewayInteractionsLoader.ts @@ -8,6 +8,7 @@ import { stripTrailingSlash, InteractionsLoaderError } from '@warp'; +import { Err } from '@warp/utils'; import 'redstone-isomorphic'; interface Paging { @@ -125,15 +126,7 @@ export class WarpGatewayInteractionsLoader implements InteractionsLoader { this.logger.error(error.body.message); } const errorMessage = `Unable to retrieve transactions. Redstone gateway responded with status ${error.status}.`; - switch (error.status) { - case 504: - throw new InteractionsLoaderError('BadGatewayResponse504', errorMessage, error); - case 500: - throw new InteractionsLoaderError('BadGatewayResponse500', errorMessage, error); - - default: - throw new InteractionsLoaderError('BadGatewayResponse', errorMessage, error); - } + throw new InteractionsLoaderError({ type: 'BadGatewayResponse', status: error.status }, errorMessage, error); }); totalPages = response.paging.pages; diff --git a/src/utils/CustomError.ts b/src/utils/CustomError.ts index ee255568..541b8e7f 100644 --- a/src/utils/CustomError.ts +++ b/src/utils/CustomError.ts @@ -1,6 +1,14 @@ -export class CustomError extends Error { - constructor(public kind: T, message?: string, public originalError?: unknown) { - super(`${kind}${message ? `: ${message}` : ''}`); +/** + * A helper type to avoid having to type `{ type: "..." }` for every error detail types. + */ +export type Err = { 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); }