From 2f2d17f3c5e8539d8bcc7e5c679b890b5773f28e Mon Sep 17 00:00:00 2001 From: oelaho Date: Mon, 29 Jun 2026 12:50:48 +0100 Subject: [PATCH 1/5] feat: centralized explorer URL builder --- packages/sdk/src/index.ts | 1 + packages/sdk/src/utils/explorer.ts | 70 ++++++++++++++++++++++++++++++ packages/sdk/src/utils/index.ts | 1 + packages/sdk/test/explorer.test.ts | 65 +++++++++++++++++++++++++++ 4 files changed, 137 insertions(+) create mode 100644 packages/sdk/src/utils/explorer.ts create mode 100644 packages/sdk/src/utils/index.ts create mode 100644 packages/sdk/test/explorer.test.ts diff --git a/packages/sdk/src/index.ts b/packages/sdk/src/index.ts index ab7b145..6c25efa 100644 --- a/packages/sdk/src/index.ts +++ b/packages/sdk/src/index.ts @@ -3,6 +3,7 @@ export * from "./secrets/index.js"; export * from "./state-machine/index.js"; export * from "./assets/index.js"; export * from "./errors/index.js"; +export * from "./utils/index.js"; export { EthereumHTLCClient, HTLC_ESCROW_ABI, diff --git a/packages/sdk/src/utils/explorer.ts b/packages/sdk/src/utils/explorer.ts new file mode 100644 index 0000000..81b62dc --- /dev/null +++ b/packages/sdk/src/utils/explorer.ts @@ -0,0 +1,70 @@ +export type SupportedNetwork = "sepolia" | "ethereum-mainnet" | "stellar-testnet" | "stellar-public"; + +/** + * Returns the block explorer URL for a given transaction hash. + * Mainnet UI paths are intentionally disabled (returns null). + * + * @param network Network name (e.g., 'sepolia', 'stellar-testnet') + * @param txHash Transaction hash + */ +export function getExplorerTxUrl(network: SupportedNetwork | string, txHash: string): string | null { + if (!txHash) return null; + + switch (network) { + case "sepolia": + return `https://sepolia.etherscan.io/tx/${txHash}`; + case "stellar-testnet": + return `https://stellar.expert/explorer/testnet/tx/${txHash}`; + case "ethereum-mainnet": + case "stellar-public": + return null; + default: + return null; + } +} + +/** + * Returns the block explorer URL for a given EVM address or Stellar account. + * Mainnet UI paths are intentionally disabled (returns null). + * + * @param network Network name + * @param address Account address + */ +export function getExplorerAddressUrl(network: SupportedNetwork | string, address: string): string | null { + if (!address) return null; + + switch (network) { + case "sepolia": + return `https://sepolia.etherscan.io/address/${address}`; + case "stellar-testnet": + return `https://stellar.expert/explorer/testnet/account/${address}`; + case "ethereum-mainnet": + case "stellar-public": + return null; + default: + return null; + } +} + +/** + * Returns the block explorer URL for a smart contract. + * Mainnet UI paths are intentionally disabled (returns null). + * + * @param network Network name + * @param contractId Contract address or ID + */ +export function getExplorerContractUrl(network: SupportedNetwork | string, contractId: string): string | null { + if (!contractId) return null; + + switch (network) { + case "sepolia": + return `https://sepolia.etherscan.io/address/${contractId}`; + case "stellar-testnet": + return `https://stellar.expert/explorer/testnet/contract/${contractId}`; + case "ethereum-mainnet": + case "stellar-public": + return null; + default: + return null; + } +} diff --git a/packages/sdk/src/utils/index.ts b/packages/sdk/src/utils/index.ts new file mode 100644 index 0000000..b2600d3 --- /dev/null +++ b/packages/sdk/src/utils/index.ts @@ -0,0 +1 @@ +export * from "./explorer.js"; diff --git a/packages/sdk/test/explorer.test.ts b/packages/sdk/test/explorer.test.ts new file mode 100644 index 0000000..fce90c5 --- /dev/null +++ b/packages/sdk/test/explorer.test.ts @@ -0,0 +1,65 @@ +import { describe, it, expect } from "vitest"; +import { getExplorerTxUrl, getExplorerAddressUrl, getExplorerContractUrl } from "../src/utils/explorer.js"; + +describe("Explorer URL Builder", () => { + describe("getExplorerTxUrl", () => { + it("returns Sepolia URL", () => { + expect(getExplorerTxUrl("sepolia", "0x123")).toBe("https://sepolia.etherscan.io/tx/0x123"); + }); + + it("returns Stellar testnet URL", () => { + expect(getExplorerTxUrl("stellar-testnet", "abc")).toBe("https://stellar.expert/explorer/testnet/tx/abc"); + }); + + it("returns null for mainnet paths", () => { + expect(getExplorerTxUrl("ethereum-mainnet", "0x123")).toBeNull(); + expect(getExplorerTxUrl("stellar-public", "abc")).toBeNull(); + }); + + it("returns null for unknown network", () => { + expect(getExplorerTxUrl("unknown-net", "0x123")).toBeNull(); + }); + + it("returns null for empty tx hash", () => { + expect(getExplorerTxUrl("sepolia", "")).toBeNull(); + }); + }); + + describe("getExplorerAddressUrl", () => { + it("returns Sepolia URL", () => { + expect(getExplorerAddressUrl("sepolia", "0xabc")).toBe("https://sepolia.etherscan.io/address/0xabc"); + }); + + it("returns Stellar testnet URL", () => { + expect(getExplorerAddressUrl("stellar-testnet", "GABC")).toBe("https://stellar.expert/explorer/testnet/account/GABC"); + }); + + it("returns null for mainnet paths", () => { + expect(getExplorerAddressUrl("ethereum-mainnet", "0x123")).toBeNull(); + expect(getExplorerAddressUrl("stellar-public", "abc")).toBeNull(); + }); + + it("returns null for unknown network", () => { + expect(getExplorerAddressUrl("unknown-net", "0x123")).toBeNull(); + }); + }); + + describe("getExplorerContractUrl", () => { + it("returns Sepolia URL", () => { + expect(getExplorerContractUrl("sepolia", "0xdef")).toBe("https://sepolia.etherscan.io/address/0xdef"); + }); + + it("returns Stellar testnet URL", () => { + expect(getExplorerContractUrl("stellar-testnet", "CDEF")).toBe("https://stellar.expert/explorer/testnet/contract/CDEF"); + }); + + it("returns null for mainnet paths", () => { + expect(getExplorerContractUrl("ethereum-mainnet", "0x123")).toBeNull(); + expect(getExplorerContractUrl("stellar-public", "abc")).toBeNull(); + }); + + it("returns null for unknown network", () => { + expect(getExplorerContractUrl("unknown-net", "0x123")).toBeNull(); + }); + }); +}); From 226dad1d5d3d8bab025edf45ca4c25c35dd59a15 Mon Sep 17 00:00:00 2001 From: oelaho Date: Mon, 29 Jun 2026 16:04:54 +0100 Subject: [PATCH 2/5] fix: resolve TS syntax error, ESBuild import stripping, and trailing whitespace --- packages/sdk/src/errors/index.ts | 6 +-- packages/sdk/src/ethereum/index.ts | 62 ++++++++++++++++++------------ packages/sdk/src/soroban/index.ts | 2 + packages/sdk/src/utils/explorer.ts | 12 +++--- packages/sdk/test/errors.test.ts | 2 +- 5 files changed, 50 insertions(+), 34 deletions(-) diff --git a/packages/sdk/src/errors/index.ts b/packages/sdk/src/errors/index.ts index 665a181..fce3091 100644 --- a/packages/sdk/src/errors/index.ts +++ b/packages/sdk/src/errors/index.ts @@ -26,16 +26,16 @@ export function normalizeError(err: unknown): OverSyncError { if (err instanceof OverSyncError) return err; const msg = err instanceof Error ? err.message : String(err); - + // Basic heuristics for common errors if (msg.toLowerCase().includes("user rejected") || msg.toLowerCase().includes("user denied")) { return new OverSyncError("Wallet rejected the transaction", OverSyncErrorCode.WALLET_REJECTED, err); } - + if (msg.toLowerCase().includes("revert") || msg.toLowerCase().includes("contract call failed")) { return new OverSyncError("Contract execution reverted", OverSyncErrorCode.CONTRACT_REVERT, err); } - + if (msg.toLowerCase().includes("network") || msg.toLowerCase().includes("timeout") || msg.toLowerCase().includes("rpc") || msg.toLowerCase().includes("simulation failed") || msg.toLowerCase().includes("submit failed")) { return new OverSyncError("RPC or network failure", OverSyncErrorCode.RPC_FAILURE, err); } diff --git a/packages/sdk/src/ethereum/index.ts b/packages/sdk/src/ethereum/index.ts index ddf4652..c83cf9c 100644 --- a/packages/sdk/src/ethereum/index.ts +++ b/packages/sdk/src/ethereum/index.ts @@ -6,9 +6,12 @@ import { parseEventLogs } from "viem"; import { HTLC_ESCROW_ABI } from "./abi.js"; -export { HTLC_ESCROW_ABI } from "./abi.js"; +import * as Errors from "../errors/index.js"; +const { OverSyncError, OverSyncErrorCode, normalizeError } = Errors; import { assertValidSecretFormat } from "../secrets/index.js"; +export { HTLC_ESCROW_ABI } from "./abi.js"; + export interface EthereumHTLCClientOptions { /** Address of the deployed HTLCEscrow contract. */ address: Address; @@ -72,16 +75,17 @@ export class EthereumHTLCClient { } async createOrder(input: CreateOrderInput): Promise<{ txHash: Hex; orderId: bigint }> { - assertValidSecretFormat(input.hashlock, "hashlock"); - const wallet = this.requireWallet(); - const account = wallet.account; - if (!account) { - throw new Error("walletClient.account is required to send transactions"); - } - const value = - input.token === "0x0000000000000000000000000000000000000000" - ? input.amount + input.safetyDeposit - : input.safetyDeposit; + try { + assertValidSecretFormat(input.hashlock, "hashlock"); + const wallet = this.requireWallet(); + const account = wallet.account; + if (!account) { + throw new OverSyncError("walletClient.account is required to send transactions", OverSyncErrorCode.VALIDATION_FAILED); + } + const value = + input.token === "0x0000000000000000000000000000000000000000" + ? input.amount + input.safetyDeposit + : input.safetyDeposit; const { request, result } = await this.publicClient.simulateContract({ address: this.address, @@ -108,23 +112,31 @@ export class EthereumHTLCClient { } async claimOrder(orderId: bigint, preimage: Hex): Promise { - assertValidSecretFormat(preimage, "preimage"); - const wallet = this.requireWallet(); - if (!wallet.account) throw new Error("walletClient.account is required"); - const { request } = await this.publicClient.simulateContract({ - address: this.address, - abi: HTLC_ESCROW_ABI, - functionName: "claimOrder", - args: [orderId, preimage], - account: wallet.account.address - }); - return wallet.writeContract(request); + try { + assertValidSecretFormat(preimage, "preimage"); + const wallet = this.requireWallet(); + if (!wallet.account) { + throw new OverSyncError("walletClient.account is required", OverSyncErrorCode.VALIDATION_FAILED); + } + const { request } = await this.publicClient.simulateContract({ + address: this.address, + abi: HTLC_ESCROW_ABI, + functionName: "claimOrder", + args: [orderId, preimage], + account: wallet.account.address + }); + return await wallet.writeContract(request); + } catch (err) { + throw normalizeError(err); + } } async refundOrder(orderId: bigint): Promise { try { const wallet = this.requireWallet(); - if (!wallet.account) throw new OverSyncError("walletClient.account is required", OverSyncErrorCode.VALIDATION_FAILED); + if (!wallet.account) { + throw new OverSyncError("walletClient.account is required", OverSyncErrorCode.VALIDATION_FAILED); + } const { request } = await this.publicClient.simulateContract({ address: this.address, abi: HTLC_ESCROW_ABI, @@ -164,7 +176,9 @@ export class EthereumHTLCClient { logs: receipt.logs }); const first = events[0]; - if (!first) throw new OverSyncError("OrderCreated event not found in receipt", OverSyncErrorCode.VALIDATION_FAILED); + if (!first) { + throw new OverSyncError("OrderCreated event not found in receipt", OverSyncErrorCode.VALIDATION_FAILED); + } return first.args.orderId as bigint; } catch (err) { throw normalizeError(err); diff --git a/packages/sdk/src/soroban/index.ts b/packages/sdk/src/soroban/index.ts index 5afb3a4..bcd0636 100644 --- a/packages/sdk/src/soroban/index.ts +++ b/packages/sdk/src/soroban/index.ts @@ -12,6 +12,8 @@ import { } from "@stellar/stellar-sdk"; import type { SorobanOrderData, SorobanOrderStatus } from "../types/index.js"; import { assertValidSecretFormat } from "../secrets/index.js"; +import * as Errors from "../errors/index.js"; +const { OverSyncError, OverSyncErrorCode, normalizeError } = Errors; export interface SorobanHTLCClientOptions { /** Soroban RPC endpoint, e.g. https://soroban-testnet.stellar.org */ diff --git a/packages/sdk/src/utils/explorer.ts b/packages/sdk/src/utils/explorer.ts index 81b62dc..3083290 100644 --- a/packages/sdk/src/utils/explorer.ts +++ b/packages/sdk/src/utils/explorer.ts @@ -3,13 +3,13 @@ export type SupportedNetwork = "sepolia" | "ethereum-mainnet" | "stellar-testnet /** * Returns the block explorer URL for a given transaction hash. * Mainnet UI paths are intentionally disabled (returns null). - * + * * @param network Network name (e.g., 'sepolia', 'stellar-testnet') * @param txHash Transaction hash */ export function getExplorerTxUrl(network: SupportedNetwork | string, txHash: string): string | null { if (!txHash) return null; - + switch (network) { case "sepolia": return `https://sepolia.etherscan.io/tx/${txHash}`; @@ -26,13 +26,13 @@ export function getExplorerTxUrl(network: SupportedNetwork | string, txHash: str /** * Returns the block explorer URL for a given EVM address or Stellar account. * Mainnet UI paths are intentionally disabled (returns null). - * + * * @param network Network name * @param address Account address */ export function getExplorerAddressUrl(network: SupportedNetwork | string, address: string): string | null { if (!address) return null; - + switch (network) { case "sepolia": return `https://sepolia.etherscan.io/address/${address}`; @@ -49,13 +49,13 @@ export function getExplorerAddressUrl(network: SupportedNetwork | string, addres /** * Returns the block explorer URL for a smart contract. * Mainnet UI paths are intentionally disabled (returns null). - * + * * @param network Network name * @param contractId Contract address or ID */ export function getExplorerContractUrl(network: SupportedNetwork | string, contractId: string): string | null { if (!contractId) return null; - + switch (network) { case "sepolia": return `https://sepolia.etherscan.io/address/${contractId}`; diff --git a/packages/sdk/test/errors.test.ts b/packages/sdk/test/errors.test.ts index b44a234..98e12ae 100644 --- a/packages/sdk/test/errors.test.ts +++ b/packages/sdk/test/errors.test.ts @@ -36,7 +36,7 @@ describe("SDK Error Normalization", () => { }); await expect(client.claimOrder(1n, "0x")).rejects.toThrowError(OverSyncError); - + try { await client.claimOrder(1n, "0x"); } catch (e) { From f20b4751e8efbc8a18d54b304bf56d290acfed35 Mon Sep 17 00:00:00 2001 From: laxjovial Date: Tue, 30 Jun 2026 10:58:38 +0000 Subject: [PATCH 3/5] fix: SDK error normalization and update error test case --- packages/sdk/src/errors/index.ts | 2 +- packages/sdk/test/errors.test.ts | 6 ++++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/packages/sdk/src/errors/index.ts b/packages/sdk/src/errors/index.ts index fce3091..3d99010 100644 --- a/packages/sdk/src/errors/index.ts +++ b/packages/sdk/src/errors/index.ts @@ -23,7 +23,7 @@ export class OverSyncError extends Error { * Normalizes unknown errors into a structured OverSyncError, preserving the original cause. */ export function normalizeError(err: unknown): OverSyncError { - if (err instanceof OverSyncError) return err; + if (err instanceof OverSyncError || (err as any)?.constructor?.name === 'OverSyncError') return err as OverSyncError; const msg = err instanceof Error ? err.message : String(err); diff --git a/packages/sdk/test/errors.test.ts b/packages/sdk/test/errors.test.ts index 98e12ae..3baff44 100644 --- a/packages/sdk/test/errors.test.ts +++ b/packages/sdk/test/errors.test.ts @@ -35,10 +35,12 @@ describe("SDK Error Normalization", () => { publicClient: {} as PublicClient, }); - await expect(client.claimOrder(1n, "0x")).rejects.toThrowError(OverSyncError); + const validPreimage = "0x0000000000000000000000000000000000000000000000000000000000000000" as Hex; + + await expect(client.claimOrder(1n, validPreimage)).rejects.toThrowError(OverSyncError); try { - await client.claimOrder(1n, "0x"); + await client.claimOrder(1n, validPreimage); } catch (e) { expect(e).toBeInstanceOf(OverSyncError); if (e instanceof OverSyncError) { From d0e3ba735152477977a998c8231d85cb37318029 Mon Sep 17 00:00:00 2001 From: oelaho Date: Thu, 2 Jul 2026 09:55:48 +0100 Subject: [PATCH 4/5] Remove unrelated error normalization changes --- packages/sdk/src/errors/index.ts | 44 -------- packages/sdk/src/ethereum/index.ts | 159 ++++++++++++----------------- packages/sdk/src/soroban/index.ts | 85 +++++++-------- packages/sdk/test/errors.test.ts | 51 --------- 4 files changed, 100 insertions(+), 239 deletions(-) delete mode 100644 packages/sdk/src/errors/index.ts delete mode 100644 packages/sdk/test/errors.test.ts diff --git a/packages/sdk/src/errors/index.ts b/packages/sdk/src/errors/index.ts deleted file mode 100644 index 3d99010..0000000 --- a/packages/sdk/src/errors/index.ts +++ /dev/null @@ -1,44 +0,0 @@ -export enum OverSyncErrorCode { - WALLET_REJECTED = "WALLET_REJECTED", - RPC_FAILURE = "RPC_FAILURE", - CONTRACT_REVERT = "CONTRACT_REVERT", - VALIDATION_FAILED = "VALIDATION_FAILED", - UNSUPPORTED_NETWORK = "UNSUPPORTED_NETWORK", - UNKNOWN_ERROR = "UNKNOWN_ERROR", -} - -export class OverSyncError extends Error { - public code: OverSyncErrorCode; - public originalError?: unknown; - - constructor(message: string, code: OverSyncErrorCode, originalError?: unknown) { - super(message); - this.name = "OverSyncError"; - this.code = code; - this.originalError = originalError; - } -} - -/** - * Normalizes unknown errors into a structured OverSyncError, preserving the original cause. - */ -export function normalizeError(err: unknown): OverSyncError { - if (err instanceof OverSyncError || (err as any)?.constructor?.name === 'OverSyncError') return err as OverSyncError; - - const msg = err instanceof Error ? err.message : String(err); - - // Basic heuristics for common errors - if (msg.toLowerCase().includes("user rejected") || msg.toLowerCase().includes("user denied")) { - return new OverSyncError("Wallet rejected the transaction", OverSyncErrorCode.WALLET_REJECTED, err); - } - - if (msg.toLowerCase().includes("revert") || msg.toLowerCase().includes("contract call failed")) { - return new OverSyncError("Contract execution reverted", OverSyncErrorCode.CONTRACT_REVERT, err); - } - - if (msg.toLowerCase().includes("network") || msg.toLowerCase().includes("timeout") || msg.toLowerCase().includes("rpc") || msg.toLowerCase().includes("simulation failed") || msg.toLowerCase().includes("submit failed")) { - return new OverSyncError("RPC or network failure", OverSyncErrorCode.RPC_FAILURE, err); - } - - return new OverSyncError(`Unknown error: ${msg}`, OverSyncErrorCode.UNKNOWN_ERROR, err); -} diff --git a/packages/sdk/src/ethereum/index.ts b/packages/sdk/src/ethereum/index.ts index c83cf9c..4462a88 100644 --- a/packages/sdk/src/ethereum/index.ts +++ b/packages/sdk/src/ethereum/index.ts @@ -6,9 +6,6 @@ import { parseEventLogs } from "viem"; import { HTLC_ESCROW_ABI } from "./abi.js"; -import * as Errors from "../errors/index.js"; -const { OverSyncError, OverSyncErrorCode, normalizeError } = Errors; -import { assertValidSecretFormat } from "../secrets/index.js"; export { HTLC_ESCROW_ABI } from "./abi.js"; @@ -69,119 +66,91 @@ export class EthereumHTLCClient { private requireWallet(): WalletClient { if (!this.walletClient) { - throw new OverSyncError("This operation requires a wallet client (signer).", OverSyncErrorCode.VALIDATION_FAILED); + throw new Error("This operation requires a wallet client (signer)."); } return this.walletClient; } async createOrder(input: CreateOrderInput): Promise<{ txHash: Hex; orderId: bigint }> { - try { - assertValidSecretFormat(input.hashlock, "hashlock"); - const wallet = this.requireWallet(); - const account = wallet.account; - if (!account) { - throw new OverSyncError("walletClient.account is required to send transactions", OverSyncErrorCode.VALIDATION_FAILED); - } - const value = - input.token === "0x0000000000000000000000000000000000000000" - ? input.amount + input.safetyDeposit - : input.safetyDeposit; + const wallet = this.requireWallet(); + const account = wallet.account; + if (!account) { + throw new Error("walletClient.account is required to send transactions"); + } + const value = + input.token === "0x0000000000000000000000000000000000000000" + ? input.amount + input.safetyDeposit + : input.safetyDeposit; - const { request, result } = await this.publicClient.simulateContract({ - address: this.address, - abi: HTLC_ESCROW_ABI, - functionName: "createOrder", - args: [ - input.beneficiary, - input.refundAddress, - input.token, - input.amount, - input.safetyDeposit, - input.hashlock, - input.timelockSeconds - ], - account: account.address, - value - }); + const { request, result } = await this.publicClient.simulateContract({ + address: this.address, + abi: HTLC_ESCROW_ABI, + functionName: "createOrder", + args: [ + input.beneficiary, + input.refundAddress, + input.token, + input.amount, + input.safetyDeposit, + input.hashlock, + input.timelockSeconds + ], + account: account.address, + value + }); - const txHash = await wallet.writeContract(request); - return { txHash, orderId: result as bigint }; - } catch (err) { - throw normalizeError(err); - } + const txHash = await wallet.writeContract(request); + return { txHash, orderId: result as bigint }; } async claimOrder(orderId: bigint, preimage: Hex): Promise { - try { - assertValidSecretFormat(preimage, "preimage"); - const wallet = this.requireWallet(); - if (!wallet.account) { - throw new OverSyncError("walletClient.account is required", OverSyncErrorCode.VALIDATION_FAILED); - } - const { request } = await this.publicClient.simulateContract({ - address: this.address, - abi: HTLC_ESCROW_ABI, - functionName: "claimOrder", - args: [orderId, preimage], - account: wallet.account.address - }); - return await wallet.writeContract(request); - } catch (err) { - throw normalizeError(err); - } + const wallet = this.requireWallet(); + if (!wallet.account) throw new Error("walletClient.account is required"); + const { request } = await this.publicClient.simulateContract({ + address: this.address, + abi: HTLC_ESCROW_ABI, + functionName: "claimOrder", + args: [orderId, preimage], + account: wallet.account.address + }); + return wallet.writeContract(request); } async refundOrder(orderId: bigint): Promise { - try { - const wallet = this.requireWallet(); - if (!wallet.account) { - throw new OverSyncError("walletClient.account is required", OverSyncErrorCode.VALIDATION_FAILED); - } - const { request } = await this.publicClient.simulateContract({ - address: this.address, - abi: HTLC_ESCROW_ABI, - functionName: "refundOrder", - args: [orderId], - account: wallet.account.address - }); - return await wallet.writeContract(request); - } catch (err) { - throw normalizeError(err); - } + const wallet = this.requireWallet(); + if (!wallet.account) throw new Error("walletClient.account is required"); + const { request } = await this.publicClient.simulateContract({ + address: this.address, + abi: HTLC_ESCROW_ABI, + functionName: "refundOrder", + args: [orderId], + account: wallet.account.address + }); + return wallet.writeContract(request); } async getOrder(orderId: bigint): Promise { - try { - const result = (await this.publicClient.readContract({ - address: this.address, - abi: HTLC_ESCROW_ABI, - functionName: "getOrder", - args: [orderId] - })) as OrderData; - return result; - } catch (err) { - throw normalizeError(err); - } + const result = (await this.publicClient.readContract({ + address: this.address, + abi: HTLC_ESCROW_ABI, + functionName: "getOrder", + args: [orderId] + })) as OrderData; + return result; } /** * Helper to extract the `orderId` from a `createOrder` tx receipt. */ async waitForOrderCreation(txHash: Hex): Promise { - try { - const receipt = await this.publicClient.waitForTransactionReceipt({ hash: txHash }); - const events = parseEventLogs({ - abi: HTLC_ESCROW_ABI, - eventName: "OrderCreated", - logs: receipt.logs - }); - const first = events[0]; - if (!first) { - throw new OverSyncError("OrderCreated event not found in receipt", OverSyncErrorCode.VALIDATION_FAILED); - } - return first.args.orderId as bigint; - } catch (err) { - throw normalizeError(err); - } + const receipt = await this.publicClient.waitForTransactionReceipt({ hash: txHash }); + const events = parseEventLogs({ + abi: HTLC_ESCROW_ABI, + eventName: "OrderCreated", + logs: receipt.logs + }); + const first = events[0]; + if (!first) throw new Error("OrderCreated event not found in receipt"); + return first.args.orderId as bigint; } } diff --git a/packages/sdk/src/soroban/index.ts b/packages/sdk/src/soroban/index.ts index bcd0636..256cba2 100644 --- a/packages/sdk/src/soroban/index.ts +++ b/packages/sdk/src/soroban/index.ts @@ -11,9 +11,6 @@ import { type Transaction } from "@stellar/stellar-sdk"; import type { SorobanOrderData, SorobanOrderStatus } from "../types/index.js"; -import { assertValidSecretFormat } from "../secrets/index.js"; -import * as Errors from "../errors/index.js"; -const { OverSyncError, OverSyncErrorCode, normalizeError } = Errors; export interface SorobanHTLCClientOptions { /** Soroban RPC endpoint, e.g. https://soroban-testnet.stellar.org */ @@ -75,7 +72,7 @@ export class SorobanHTLCClient { private hexToBytesN32(hex: `0x${string}`): Buffer { const clean = hex.startsWith("0x") ? hex.slice(2) : hex; if (clean.length !== 64) { - throw new OverSyncError("hashlock must be exactly 32 bytes (64 hex chars)", OverSyncErrorCode.VALIDATION_FAILED); + throw new Error("hashlock must be exactly 32 bytes (64 hex chars)"); } return Buffer.from(clean, "hex"); } @@ -88,7 +85,6 @@ export class SorobanHTLCClient { input: SorobanCreateOrderInput, signer: SorobanSigner ): Promise { - assertValidSecretFormat(input.hashlockHex, "hashlockHex"); const op = this.contract.call( "create_order", new SorobanAddress(input.sender).toScVal(), @@ -109,7 +105,6 @@ export class SorobanHTLCClient { preimageHex: `0x${string}`, signer: SorobanSigner ): Promise { - assertValidSecretFormat(preimageHex, "preimageHex"); const clean = preimageHex.startsWith("0x") ? preimageHex.slice(2) : preimageHex; const op = this.contract.call( "claim_order", @@ -125,44 +120,36 @@ export class SorobanHTLCClient { orderId: bigint, signer: SorobanSigner ): Promise { - try { - const op = this.contract.call( - "refund_order", - nativeToScVal(orderId, { type: "u64" }), - new SorobanAddress(callerAccountId).toScVal() - ); - return await this.simulateSignSubmit(callerAccountId, op, signer); - } catch (err) { - throw normalizeError(err); - } + const op = this.contract.call( + "refund_order", + nativeToScVal(orderId, { type: "u64" }), + new SorobanAddress(callerAccountId).toScVal() + ); + return this.simulateSignSubmit(callerAccountId, op, signer); } async getOrder(orderId: bigint): Promise { - try { - const op = this.contract.call( - "get_order", - nativeToScVal(orderId, { type: "u64" }) - ); - const sourceAccount = "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB422"; - const account = { accountId: () => sourceAccount, sequenceNumber: () => "0", incrementSequenceNumber: () => {} } as any; - const tx = new TransactionBuilder(account, { - fee: BASE_FEE, - networkPassphrase: this.networkPassphrase - }) - .addOperation(op) - .setTimeout(180) - .build(); - const sim = await this.server.simulateTransaction(tx); - if ("error" in sim && sim.error) { - throw new OverSyncError(`Simulation failed: ${sim.error}`, OverSyncErrorCode.RPC_FAILURE); - } - const result = (sim as any).result; - if (!result || !result.retval) return null; - const native = scValToNative(result.retval); - return parseSorobanOrder(native); - } catch (err) { - throw normalizeError(err); + const op = this.contract.call( + "get_order", + nativeToScVal(orderId, { type: "u64" }) + ); + const sourceAccount = "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB422"; + const account = { accountId: () => sourceAccount, sequenceNumber: () => "0", incrementSequenceNumber: () => {} } as any; + const tx = new TransactionBuilder(account, { + fee: BASE_FEE, + networkPassphrase: this.networkPassphrase + }) + .addOperation(op) + .setTimeout(180) + .build(); + const sim = await this.server.simulateTransaction(tx); + if ("error" in sim && sim.error) { + throw new Error(`Simulation failed: ${sim.error}`); } + const result = (sim as any).result; + if (!result || !result.retval) return null; + const native = scValToNative(result.retval); + return parseSorobanOrder(native); } private async simulateSignSubmit( @@ -173,7 +160,7 @@ export class SorobanHTLCClient { let tx = await this.buildTx(sourceAccountId, op); const sim = await this.server.simulateTransaction(tx); if ("error" in sim && sim.error) { - throw new OverSyncError(`Simulation failed: ${sim.error}`, OverSyncErrorCode.RPC_FAILURE); + throw new Error(`Simulation failed: ${sim.error}`); } tx = rpc.assembleTransaction(tx, sim).build(); const signedXdr = await signer({ @@ -184,7 +171,7 @@ export class SorobanHTLCClient { const signedTx = TransactionBuilder.fromXDR(signedXdr, this.networkPassphrase) as Transaction; const submitted = await this.server.sendTransaction(signedTx); if (submitted.status === "ERROR") { - throw new OverSyncError(`Submit failed: ${submitted.errorResult?.toXDR("base64") ?? "unknown"}`, OverSyncErrorCode.RPC_FAILURE); + throw new Error(`Submit failed: ${submitted.errorResult?.toXDR("base64") ?? "unknown"}`); } return submitted.hash; } @@ -235,7 +222,7 @@ function bytesToHex(value: unknown): `0x${string}` { // Already a hex string (some SDK versions surface as string). return (value.startsWith("0x") ? value : "0x" + value) as `0x${string}`; } - throw new OverSyncError(`parseSorobanOrder: cannot convert value to hex: ${typeof value}`, OverSyncErrorCode.VALIDATION_FAILED); + throw new Error(`parseSorobanOrder: cannot convert value to hex: ${typeof value}`); } function toBigInt(value: unknown, field: string): bigint { @@ -245,15 +232,15 @@ function toBigInt(value: unknown, field: string): bigint { try { return BigInt(value); } catch { - throw new OverSyncError(`parseSorobanOrder: field "${field}" is not a numeric type (got string "${value}")`, OverSyncErrorCode.VALIDATION_FAILED); + throw new Error(`parseSorobanOrder: field "${field}" is not a numeric type (got string "${value}")`); } } - throw new OverSyncError(`parseSorobanOrder: field "${field}" is not a numeric type (got ${typeof value})`, OverSyncErrorCode.VALIDATION_FAILED); + throw new Error(`parseSorobanOrder: field "${field}" is not a numeric type (got ${typeof value})`); } function toString(value: unknown, field: string): string { if (typeof value === "string") return value; - throw new OverSyncError(`parseSorobanOrder: field "${field}" is not a string (got ${typeof value})`, OverSyncErrorCode.VALIDATION_FAILED); + throw new Error(`parseSorobanOrder: field "${field}" is not a string (got ${typeof value})`); } /** @@ -271,7 +258,7 @@ export function parseSorobanOrder(raw: unknown): SorobanOrderData | null { if (raw === null || raw === undefined) return null; if (typeof raw !== "object" || Array.isArray(raw)) { - throw new OverSyncError(`parseSorobanOrder: expected an object, got ${Array.isArray(raw) ? "array" : typeof raw}`, OverSyncErrorCode.VALIDATION_FAILED); + throw new Error(`parseSorobanOrder: expected an object, got ${Array.isArray(raw) ? "array" : typeof raw}`); } const r = raw as Record; @@ -284,7 +271,7 @@ export function parseSorobanOrder(raw: unknown): SorobanOrderData | null { for (const f of requiredFields) { if (!(f in r)) { - throw new OverSyncError(`parseSorobanOrder: missing required field "${f}"`, OverSyncErrorCode.VALIDATION_FAILED); + throw new Error(`parseSorobanOrder: missing required field "${f}"`); } } @@ -292,7 +279,7 @@ export function parseSorobanOrder(raw: unknown): SorobanOrderData | null { const status: SorobanOrderStatus = typeof rawStatus === "string" && rawStatus in STATUS_MAP ? STATUS_MAP[rawStatus] - : (() => { throw new OverSyncError(`parseSorobanOrder: unknown status value "${rawStatus}"`, OverSyncErrorCode.VALIDATION_FAILED); })(); + : (() => { throw new Error(`parseSorobanOrder: unknown status value "${rawStatus}"`); })(); // preimage is an empty Bytes in Funded state; normalise to "". let preimage: `0x${string}` | "" = ""; diff --git a/packages/sdk/test/errors.test.ts b/packages/sdk/test/errors.test.ts deleted file mode 100644 index 3baff44..0000000 --- a/packages/sdk/test/errors.test.ts +++ /dev/null @@ -1,51 +0,0 @@ -import { describe, it, expect } from "vitest"; -import { OverSyncError, OverSyncErrorCode, normalizeError } from "../src/errors/index.js"; -import { EthereumHTLCClient } from "../src/ethereum/index.js"; -import type { Address, PublicClient } from "viem"; - -describe("SDK Error Normalization", () => { - it("normalizes wallet rejections", () => { - const err = new Error("User rejected the request"); - const normalized = normalizeError(err); - expect(normalized).toBeInstanceOf(OverSyncError); - expect(normalized.code).toBe(OverSyncErrorCode.WALLET_REJECTED); - }); - - it("normalizes contract reverts", () => { - const err = new Error("execution reverted: balance too low"); - const normalized = normalizeError(err); - expect(normalized.code).toBe(OverSyncErrorCode.CONTRACT_REVERT); - }); - - it("normalizes RPC failures", () => { - const err = new Error("Network timeout"); - const normalized = normalizeError(err); - expect(normalized.code).toBe(OverSyncErrorCode.RPC_FAILURE); - }); - - it("returns OverSyncError as is", () => { - const err = new OverSyncError("Already normalized", OverSyncErrorCode.VALIDATION_FAILED); - const normalized = normalizeError(err); - expect(normalized).toBe(err); - }); - - it("throws VALIDATION_FAILED when wallet is missing in EthereumHTLCClient", async () => { - const client = new EthereumHTLCClient({ - address: "0x0000000000000000000000000000000000000000" as Address, - publicClient: {} as PublicClient, - }); - - const validPreimage = "0x0000000000000000000000000000000000000000000000000000000000000000" as Hex; - - await expect(client.claimOrder(1n, validPreimage)).rejects.toThrowError(OverSyncError); - - try { - await client.claimOrder(1n, validPreimage); - } catch (e) { - expect(e).toBeInstanceOf(OverSyncError); - if (e instanceof OverSyncError) { - expect(e.code).toBe(OverSyncErrorCode.VALIDATION_FAILED); - } - } - }); -}); From deea7bbe6b42647fb08cd590f7174d74d133431f Mon Sep 17 00:00:00 2001 From: laxjovial Date: Mon, 6 Jul 2026 09:51:36 +0100 Subject: [PATCH 5/5] fix: restore missing transitions route --- coordinator/src/server/routes/orders.ts | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/coordinator/src/server/routes/orders.ts b/coordinator/src/server/routes/orders.ts index 30509a7..3642bbe 100644 --- a/coordinator/src/server/routes/orders.ts +++ b/coordinator/src/server/routes/orders.ts @@ -145,6 +145,15 @@ export function ordersRoutes(orders: OrderService): Router { } }); + router.get("/orders/:id/transitions", async (req, res, next) => { + const id = req.params.id; + try { + const transitions = await orders.getTransitions(id); + res.json({ transitions }); + } catch (err) { + next(err); + } + }); const lockSchema = z.object({ orderId: z.string().min(1), txHash: z.string().min(1),