Skip to content
Merged
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
3 changes: 3 additions & 0 deletions contrib/examples/centralized-error-reporting-hook/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# Centralized Error Reporting Hook (#251)

Allows consumer applications to register an `onError` hook to capture and report uncaught SDK and RPC errors.
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { describe, it, expect, vi } from "vitest";
import { ClientWithErrorReporting } from "./centralized-error-reporting-hook";

describe("Issue #251 — Centralized Error Reporting Hook", () => {
it("calls onError when an operation fails", async () => {
const onError = vi.fn();
const client = new ClientWithErrorReporting(onError);

await expect(
client.execute(async () => {
throw new Error("RPC failure");
}, { method: "sendTransaction" })
).rejects.toThrow("RPC failure");

expect(onError).toHaveBeenCalledTimes(1);
expect(onError).toHaveBeenCalledWith(expect.any(Error), { method: "sendTransaction" });
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
/**
* Issue #251: Centralized Error Reporting Hook Interface.
*/

export type ErrorReporterHook = (error: Error, context?: Record<string, unknown>) => void;

export class ClientWithErrorReporting {
constructor(private readonly onError?: ErrorReporterHook) {}

async execute<T>(fn: () => Promise<T>, context?: Record<string, unknown>): Promise<T> {
try {
return await fn();
} catch (err) {
const error = err instanceof Error ? err : new Error(String(err));
this.onError?.(error, context);
throw error;
}
}
}
3 changes: 3 additions & 0 deletions contrib/examples/rpc-elevated-error-rate-warning/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# RPC Elevated Error Rate Warning Callback (#254)

Monitors rolling RPC error rates and triggers an alerting callback when the failure threshold is exceeded.
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { describe, it, expect, vi } from "vitest";
import { RpcErrorRateMonitor, type ErrorRateStats } from "./rpc-elevated-error-rate-warning";

describe("Issue #254 — Elevated Error Rate Warning", () => {
it("fires callback when error rate exceeds threshold", () => {
const callback = vi.fn();
const monitor = new RpcErrorRateMonitor(0.3, 10, callback);

for (let i = 0; i < 4; i++) {
monitor.record(true);
}
expect(callback).not.toHaveBeenCalled();

monitor.record(false);
monitor.record(false); // 2/6 = 33.3% > 30%

expect(callback).toHaveBeenCalledTimes(1);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
/**
* Issue #254: Configurable Elevated RPC Error Rate Callback.
*/

export interface ErrorRateStats {
errorRate: number;
totalRequests: number;
failedRequests: number;
threshold: number;
}

export type ElevatedErrorRateCallback = (stats: ErrorRateStats) => void;

export class RpcErrorRateMonitor {
private readonly history: boolean[] = [];

constructor(
private readonly threshold = 0.25,
private readonly windowSize = 20,
private readonly onElevatedErrorRate?: ElevatedErrorRateCallback
) {}

record(success: boolean): ErrorRateStats {
this.history.push(success);
if (this.history.length > this.windowSize) {
this.history.shift();
}

const totalRequests = this.history.length;
const failedRequests = this.history.filter((s) => !s).length;
const errorRate = totalRequests > 0 ? failedRequests / totalRequests : 0;

const stats: ErrorRateStats = {
errorRate,
totalRequests,
failedRequests,
threshold: this.threshold,
};

if (totalRequests >= 5 && errorRate >= this.threshold) {
this.onElevatedErrorRate?.(stats);
}

return stats;
}
}
3 changes: 3 additions & 0 deletions contrib/examples/rpc-latency-instrumentation/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# RPC Latency Instrumentation Hook (#252)

Provides duration measurement and request completion hooks for RPC calls.
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { describe, it, expect } from "vitest";
import { instrumentRpcCall, type RequestCompleteInfo } from "./rpc-latency-instrumentation";

describe("Issue #252 — RPC Latency Instrumentation", () => {
it("records duration for successful and failed calls", async () => {
const records: RequestCompleteInfo[] = [];
const hook = (info: RequestCompleteInfo) => records.push(info);

await instrumentRpcCall("simulate", async () => "ok", hook);
expect(records).toHaveLength(1);
expect(records[0].method).toBe("simulate");
expect(records[0].success).toBe(true);

await expect(
instrumentRpcCall("sendTx", async () => {
throw new Error("fail");
}, hook)
).rejects.toThrow("fail");

expect(records).toHaveLength(2);
expect(records[1].method).toBe("sendTx");
expect(records[1].success).toBe(false);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
/**
* Issue #252: RPC Latency Instrumentation Hook.
*/

export interface RequestCompleteInfo {
method: string;
durationMs: number;
success: boolean;
error?: Error;
}

export type RequestCompleteHook = (info: RequestCompleteInfo) => void;

export async function instrumentRpcCall<T>(
method: string,
fn: () => Promise<T>,
hook?: RequestCompleteHook
): Promise<T> {
const start = Date.now();
try {
const result = await fn();
hook?.({
method,
durationMs: Date.now() - start,
success: true,
});
return result;
} catch (err) {
const error = err instanceof Error ? err : new Error(String(err));
hook?.({
method,
durationMs: Date.now() - start,
success: false,
error,
});
throw error;
}
}
3 changes: 3 additions & 0 deletions contrib/examples/standardized-error-codes/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# Standardized SDK Error Codes (#249)

Defines a standardized error code naming convention across all SDK modules following the format `VELLAR_<MODULE>_<REASON>`.
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { describe, it, expect } from "vitest";
import { VellarError, VellarErrorCode } from "./standardized-error-codes";

describe("Issue #249 — Standardized Error Codes", () => {
it("enforces standardized naming format VELLAR_<MODULE>_<REASON>", () => {
const regex = /^VELLAR_[A-Z0-9]+_[A-Z0-9_]+$/;
for (const code of Object.values(VellarErrorCode)) {
expect(code).toMatch(regex);
}
});

it("constructs VellarError with code, message, and details", () => {
const err = new VellarError(
VellarErrorCode.RPC_RATE_LIMIT_EXCEEDED,
"Too many requests sent to Soroban RPC",
{ retryAfterMs: 5000 }
);

expect(err).toBeInstanceOf(Error);
expect(err).toBeInstanceOf(VellarError);
expect(err.code).toBe("VELLAR_RPC_RATE_LIMIT_EXCEEDED");
expect(err.message).toContain("[VELLAR_RPC_RATE_LIMIT_EXCEEDED]");
expect(err.details?.retryAfterMs).toBe(5000);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
/**
* Issue #249: Standardized SDK Error Codes.
* Naming convention: VELLAR_<MODULE>_<REASON>
*/

export const VellarErrorCode = {
AUTH_SESSION_EXPIRED: "VELLAR_AUTH_SESSION_EXPIRED",
AUTH_BROWSER_REQUIRED: "VELLAR_AUTH_BROWSER_REQUIRED",
AUTH_INVALID_CREDENTIALS: "VELLAR_AUTH_INVALID_CREDENTIALS",
AUTH_CHALLENGE_REPLAYED: "VELLAR_AUTH_CHALLENGE_REPLAYED",
RPC_RATE_LIMIT_EXCEEDED: "VELLAR_RPC_RATE_LIMIT_EXCEEDED",
RPC_ENDPOINT_FAILED: "VELLAR_RPC_ENDPOINT_FAILED",
RPC_TIMEOUT: "VELLAR_RPC_TIMEOUT",
RPC_INVALID_NETWORK: "VELLAR_RPC_INVALID_NETWORK",
CONFIG_INVALID: "VELLAR_CONFIG_INVALID",
VALIDATION_INVALID_ADDRESS: "VELLAR_VALIDATION_INVALID_ADDRESS",
VALIDATION_UNTRUSTED_VECTOR: "VELLAR_VALIDATION_UNTRUSTED_VECTOR",
X402_NOT_CONFIGURED: "VELLAR_X402_NOT_CONFIGURED",
X402_PAYMENT_REJECTED: "VELLAR_X402_PAYMENT_REJECTED",
X402_INVALID_CHALLENGE: "VELLAR_X402_INVALID_CHALLENGE",
X402_SIGNER_UNAUTHORIZED: "VELLAR_X402_SIGNER_UNAUTHORIZED",
} as const;

export type VellarErrorCodeType = (typeof VellarErrorCode)[keyof typeof VellarErrorCode];

export class VellarError extends Error {
public readonly code: VellarErrorCodeType;
public readonly details?: Record<string, unknown>;

constructor(code: VellarErrorCodeType, message: string, details?: Record<string, unknown>) {
super(`[${code}] ${message}`);
this.name = "VellarError";
this.code = code;
this.details = details;
Object.setPrototypeOf(this, new.target.prototype);
}
}
Loading