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), diff --git a/packages/sdk/src/soroban/index.ts b/packages/sdk/src/soroban/index.ts index bcd0636..1998ea6 100644 --- a/packages/sdk/src/soroban/index.ts +++ b/packages/sdk/src/soroban/index.ts @@ -88,7 +88,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 +108,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", diff --git a/packages/sdk/src/utils/explorer.ts b/packages/sdk/src/utils/explorer.ts new file mode 100644 index 0000000..3083290 --- /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(); + }); + }); +});