From 40231823789fe8bbd4a3dee6d1eb8a5972346ef5 Mon Sep 17 00:00:00 2001 From: Opulence Chuks Date: Mon, 1 Jun 2026 01:55:57 +0100 Subject: [PATCH] Implement comprehensive SDK error hierarchy with error codes and structured error handling --- sdk/src/errors.ts | 98 +++++++++++++++++++++++++++++++++++------------ sdk/src/utils.ts | 13 +++++-- 2 files changed, 83 insertions(+), 28 deletions(-) diff --git a/sdk/src/errors.ts b/sdk/src/errors.ts index 03c9a73..bfadf71 100644 --- a/sdk/src/errors.ts +++ b/sdk/src/errors.ts @@ -1,53 +1,103 @@ +// @bc-forge/sdk — Comprehensive Error Hierarchy + /** - * @bc-forge/sdk — Custom Error Classes + * Enumeration of standardized error codes used across the SDK. */ +export enum BcForgeErrorCode { + NETWORK_ERROR = 'NETWORK_ERROR', + SIMULATION_ERROR = 'SIMULATION_ERROR', + TRANSACTION_ERROR = 'TRANSACTION_ERROR', + CONTRACT_ERROR = 'CONTRACT_ERROR', + VALIDATION_ERROR = 'VALIDATION_ERROR', +} /** - * Base class for all SDK errors. + * Base class for all SDK errors. Provides a consistent structure including an error code, + * a human‑readable message, an optional underlying cause, and an `isRetryable` flag. */ -export class bcForgeError extends Error { - constructor(message: string) { +export class BcForgeError extends Error { + /** Unique error code identifying the error type. */ + public readonly errorCode: BcForgeErrorCode; + /** Indicates whether the operation can be safely retried. */ + public readonly isRetryable: boolean; + /** Optional underlying error or additional context. */ + public readonly cause?: any; + + constructor(message: string, errorCode: BcForgeErrorCode, isRetryable: boolean = false, cause?: any) { super(message); - this.name = 'bcForgeError'; + this.name = 'BcForgeError'; + this.errorCode = errorCode; + this.isRetryable = isRetryable; + this.cause = cause; } } /** - * Thrown when a contract simulation fails. + * Errors caused by network failures or unavailable RPC endpoints. */ -export class SimulationError extends bcForgeError { - constructor(message: string, public readonly errorDetails?: string) { - super(message); +export class NetworkError extends BcForgeError { + constructor(message: string, cause?: any) { + super(message, BcForgeErrorCode.NETWORK_ERROR, true, cause); + this.name = 'NetworkError'; + } +} + +/** + * Errors occurring during contract simulation. Includes the raw Soroban panic message if available. + */ +export class SimulationError extends BcForgeError { + public readonly panicMessage?: string; + + constructor(message: string, panicMessage?: string, cause?: any) { + super(message, BcForgeErrorCode.SIMULATION_ERROR, false, cause); this.name = 'SimulationError'; + this.panicMessage = panicMessage; + } + + /** + * Parses a Soroban panic string and extracts a concise description. + * Returns `undefined` if the input does not appear to be a panic. + */ + static parsePanic(panic: string): string | undefined { + // Typical panic format: "panic: :
" + const match = panic.match(/panic:\s*([^:]+):\s*(.*)/i); + if (match) { + const [, type, details] = match; + return `${type.trim()}: ${details.trim()}`; + } + return undefined; } } /** - * Thrown when a transaction submission fails at the RPC level. + * Errors related to transaction submission, timeout, or unexpected RPC responses. */ -export class TransactionSubmissionError extends bcForgeError { - constructor(message: string, public readonly hash?: string) { - super(message); - this.name = 'TransactionSubmissionError'; +export class TransactionError extends BcForgeError { + public readonly transactionHash?: string; + + constructor(message: string, transactionHash?: string, cause?: any) { + super(message, BcForgeErrorCode.TRANSACTION_ERROR, false, cause); + this.name = 'TransactionError'; + this.transactionHash = transactionHash; } } /** - * Thrown when a transaction is not found after polling. + * Errors originating from contract execution, such as missing methods or contract‑specific failures. */ -export class TransactionTimeoutError extends bcForgeError { - constructor(message: string, public readonly hash: string) { - super(message); - this.name = 'TransactionTimeoutError'; +export class ContractError extends BcForgeError { + constructor(message: string, cause?: any) { + super(message, BcForgeErrorCode.CONTRACT_ERROR, false, cause); + this.name = 'ContractError'; } } /** - * Thrown when an RPC call fails due to transient network issues. + * Validation errors for incorrect input values, data formats, or unsupported operations. */ -export class RPCError extends bcForgeError { - constructor(message: string, public readonly originalError?: any) { - super(message); - this.name = 'RPCError'; +export class ValidationError extends BcForgeError { + constructor(message: string, cause?: any) { + super(message, BcForgeErrorCode.VALIDATION_ERROR, false, cause); + this.name = 'ValidationError'; } } diff --git a/sdk/src/utils.ts b/sdk/src/utils.ts index 404686a..0efc57d 100644 --- a/sdk/src/utils.ts +++ b/sdk/src/utils.ts @@ -15,7 +15,7 @@ import { Keypair, } from '@stellar/stellar-sdk'; -import { SimulationError, TransactionSubmissionError, TransactionTimeoutError } from './errors'; +import { SimulationError, TransactionError, ValidationError } from './errors'; /** * Builds an `invokeHostFunction` transaction for a Soroban contract call. @@ -179,7 +179,9 @@ export async function buildUnsignedTransaction( const simulated = await server.simulateTransaction(tx); if (SorobanRpc.Api.isSimulationError(simulated)) { - throw new Error(`Simulation failed: ${simulated.error}`); + // Parse potential Soroban panic message and throw a structured SimulationError + const panicMsg = SimulationError.parsePanic(simulated.error); + throw new SimulationError('Simulation failed', panicMsg ?? simulated.error, simulated.error); } const assembled = SorobanRpc.assembleTransaction(tx, simulated).build(); @@ -243,7 +245,8 @@ export async function simulateTransaction( const simulated = await server.simulateTransaction(tx); if (SorobanRpc.Api.isSimulationError(simulated)) { - throw new Error(`Simulation failed: ${simulated.error}`); + const panicMsg = SimulationError.parsePanic(simulated.error); + throw new SimulationError('Simulation failed', panicMsg ?? simulated.error, simulated.error); } return simulated; @@ -254,6 +257,8 @@ export async function simulateTransaction( */ export function hashToScVal(hash: string | Buffer): xdr.ScVal { const buf = typeof hash === 'string' ? Buffer.from(hash, 'hex') : hash; - if (buf.length !== 32) throw new Error('Hash must be exactly 32 bytes'); + if (buf.length !== 32) { + throw new ValidationError('Hash must be exactly 32 bytes'); + } return xdr.ScVal.scvBytes(buf); }