Skip to content
Open
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
98 changes: 74 additions & 24 deletions sdk/src/errors.ts
Original file line number Diff line number Diff line change
@@ -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: <type>: <details>"
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';
}
}
13 changes: 9 additions & 4 deletions sdk/src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -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;
Expand All @@ -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);
}