diff --git a/packages/sdk/package.json b/packages/sdk/package.json index f06bf2d..bdfd7bb 100644 --- a/packages/sdk/package.json +++ b/packages/sdk/package.json @@ -29,6 +29,10 @@ "./types": { "import": "./dist/types/index.js", "types": "./dist/types/index.d.ts" + }, + "./explorers": { + "import": "./dist/explorers/index.js", + "types": "./dist/explorers/index.d.ts" } }, "scripts": { diff --git a/packages/sdk/src/explorers/index.ts b/packages/sdk/src/explorers/index.ts index 7424528..59dfc13 100644 --- a/packages/sdk/src/explorers/index.ts +++ b/packages/sdk/src/explorers/index.ts @@ -1,9 +1,10 @@ // --------------------------------------------------------------------------- // OverSync SDK — Explorer URL helpers // -// Build public block-explorer links for Ethereum and Stellar transactions -// and addresses / contracts. Future mainnet variants are gated by the -// `network` argument; invalid combinations return `null`. +// Build public block-explorer links for Ethereum and Stellar transactions, +// addresses, Stellar accounts, and Soroban contracts. Future mainnet/public +// network variants are gated by the `network` argument; invalid network or +// missing/empty inputs return `null`. // --------------------------------------------------------------------------- // --------------------------------------------------------------- @@ -30,6 +31,14 @@ const STELLAR_BASE_URLS: Record = { public: "https://stellar.expert/explorer/public", }; +// --------------------------------------------------------------- +// Validation helper +// --------------------------------------------------------------- + +function isValidInput(input: unknown): input is string { + return typeof input === "string" && input.trim().length > 0; +} + // --------------------------------------------------------------- // URL builders // --------------------------------------------------------------- @@ -40,16 +49,16 @@ const STELLAR_BASE_URLS: Record = { * @param network - Target network (`"sepolia"` or `"mainnet"`). * @param txHash - 0x-prefixed transaction hash. * @returns The full explorer URL, or `null` if the network is - * unrecognised (provides type safety at compile time, guards - * against unexpected values at runtime). + * unrecognised or input is empty/invalid. */ export function ethereumTxUrl( network: EthereumNetwork, txHash: string, ): string | null { + if (!isValidInput(txHash)) return null; const base = ETHEREUM_BASE_URLS[network]; if (!base) return null; - return `${base}/tx/${txHash}`; + return `${base}/tx/${txHash.trim()}`; } /** @@ -58,49 +67,69 @@ export function ethereumTxUrl( * @param network - Target network (`"sepolia"` or `"mainnet"`). * @param address - 0x-prefixed address. * @returns The full explorer URL, or `null` if the network is - * unrecognised. + * unrecognised or input is empty/invalid. */ export function ethereumAddressUrl( network: EthereumNetwork, address: string, ): string | null { + if (!isValidInput(address)) return null; const base = ETHEREUM_BASE_URLS[network]; if (!base) return null; - return `${base}/address/${address}`; + return `${base}/address/${address.trim()}`; } /** * Build a Stellar Expert transaction URL for the given Stellar network. * * @param network - Target network (`"testnet"` or `"public"`). - * @param txHash - Stellar transaction hash (base-64 or hex, as returned - * by the Horizon API). + * @param txHash - Stellar transaction hash. * @returns The full explorer URL, or `null` if the network is - * unrecognised. + * unrecognised or input is empty/invalid. */ export function stellarTxUrl( network: StellarNetwork, txHash: string, ): string | null { + if (!isValidInput(txHash)) return null; + const base = STELLAR_BASE_URLS[network]; + if (!base) return null; + return `${base}/tx/${txHash.trim()}`; +} + +/** + * Build a Stellar Expert account URL for the given Stellar network. + * + * @param network - Target network (`"testnet"` or `"public"`). + * @param accountId - Stellar account ID (G...). + * @returns The full explorer URL, or `null` if the network is + * unrecognised or input is empty/invalid. + */ +export function stellarAccountUrl( + network: StellarNetwork, + accountId: string, +): string | null { + if (!isValidInput(accountId)) return null; const base = STELLAR_BASE_URLS[network]; if (!base) return null; - return `${base}/tx/${txHash}`; + return `${base}/account/${accountId.trim()}`; } /** - * Build a Stellar Expert contract / account URL for the given Stellar + * Build a Stellar Expert contract / Soroban contract URL for the given Stellar * network. * * @param network - Target network (`"testnet"` or `"public"`). - * @param contractId - Stellar contract or account ID. + * @param contractId - Soroban contract ID (C...). * @returns The full explorer URL, or `null` if the network is - * unrecognised. + * unrecognised or input is empty/invalid. */ export function stellarContractUrl( network: StellarNetwork, contractId: string, ): string | null { + if (!isValidInput(contractId)) return null; const base = STELLAR_BASE_URLS[network]; if (!base) return null; - return `${base}/contract/${contractId}`; + return `${base}/contract/${contractId.trim()}`; } diff --git a/packages/sdk/test/explorers.test.ts b/packages/sdk/test/explorers.test.ts index 46376c1..f767134 100644 --- a/packages/sdk/test/explorers.test.ts +++ b/packages/sdk/test/explorers.test.ts @@ -3,6 +3,7 @@ import { ethereumTxUrl, ethereumAddressUrl, stellarTxUrl, + stellarAccountUrl, stellarContractUrl, } from "../src/explorers/index.js"; @@ -10,10 +11,11 @@ describe("explorers", () => { const TX_HASH = "0xabcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890"; const ADDRESS = "0x1234567890abcdef1234567890abcdef12345678"; const STELLAR_TX_HASH = "a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b"; - const CONTRACT_ID = "CDLZFC3SYJYDZT7K3VJ3SJQH3VJ3SJQH3VJ3SJQH3VJ3SJQH3VJ3SJQH3"; + const STELLAR_ACCOUNT_ID = "GBRPYHIL2CI3FNQ4BXLFMNDLFJUNPU2HY3ZMFXYFTRE6A6B7OD22OM4B"; + const SOROBAN_CONTRACT_ID = "CDLZFC3SYJYDZT7K3VJ3SJQH3VJ3SJQH3VJ3SJQH3VJ3SJQH3VJ3SJQH3"; // --------------------------------------------------------------- - // Ethereum — Sepolia + // Ethereum — Sepolia & Mainnet // --------------------------------------------------------------- describe("ethereumTxUrl", () => { @@ -29,9 +31,22 @@ describe("explorers", () => { ); }); + it("trims whitespace from transaction hash", () => { + expect(ethereumTxUrl("sepolia", ` ${TX_HASH} `)).toBe( + `https://sepolia.etherscan.io/tx/${TX_HASH}`, + ); + }); + it("returns null for an unrecognised network", () => { expect(ethereumTxUrl("unknown" as any, TX_HASH)).toBeNull(); }); + + it("returns null for empty or invalid txHash inputs", () => { + expect(ethereumTxUrl("sepolia", "")).toBeNull(); + expect(ethereumTxUrl("sepolia", " ")).toBeNull(); + expect(ethereumTxUrl("sepolia", null as any)).toBeNull(); + expect(ethereumTxUrl("sepolia", undefined as any)).toBeNull(); + }); }); describe("ethereumAddressUrl", () => { @@ -47,13 +62,26 @@ describe("explorers", () => { ); }); + it("trims whitespace from address", () => { + expect(ethereumAddressUrl("sepolia", ` ${ADDRESS} `)).toBe( + `https://sepolia.etherscan.io/address/${ADDRESS}`, + ); + }); + it("returns null for an unrecognised network", () => { expect(ethereumAddressUrl("unknown" as any, ADDRESS)).toBeNull(); }); + + it("returns null for empty or invalid address inputs", () => { + expect(ethereumAddressUrl("sepolia", "")).toBeNull(); + expect(ethereumAddressUrl("sepolia", " ")).toBeNull(); + expect(ethereumAddressUrl("sepolia", null as any)).toBeNull(); + expect(ethereumAddressUrl("sepolia", undefined as any)).toBeNull(); + }); }); // --------------------------------------------------------------- - // Stellar + // Stellar — Testnet & Public // --------------------------------------------------------------- describe("stellarTxUrl", () => { @@ -63,32 +91,89 @@ describe("explorers", () => { ); }); - it("builds a public (mainnet) transaction URL", () => { + it("builds a public (mainnet) transaction URL placeholder", () => { expect(stellarTxUrl("public", STELLAR_TX_HASH)).toBe( `https://stellar.expert/explorer/public/tx/${STELLAR_TX_HASH}`, ); }); + it("trims whitespace from txHash", () => { + expect(stellarTxUrl("testnet", ` ${STELLAR_TX_HASH} `)).toBe( + `https://stellar.expert/explorer/testnet/tx/${STELLAR_TX_HASH}`, + ); + }); + it("returns null for an unrecognised network", () => { expect(stellarTxUrl("unknown" as any, STELLAR_TX_HASH)).toBeNull(); }); + + it("returns null for empty or invalid txHash inputs", () => { + expect(stellarTxUrl("testnet", "")).toBeNull(); + expect(stellarTxUrl("testnet", " ")).toBeNull(); + expect(stellarTxUrl("testnet", null as any)).toBeNull(); + expect(stellarTxUrl("testnet", undefined as any)).toBeNull(); + }); + }); + + describe("stellarAccountUrl", () => { + it("builds a testnet account URL", () => { + expect(stellarAccountUrl("testnet", STELLAR_ACCOUNT_ID)).toBe( + `https://stellar.expert/explorer/testnet/account/${STELLAR_ACCOUNT_ID}`, + ); + }); + + it("builds a public (mainnet) account URL placeholder", () => { + expect(stellarAccountUrl("public", STELLAR_ACCOUNT_ID)).toBe( + `https://stellar.expert/explorer/public/account/${STELLAR_ACCOUNT_ID}`, + ); + }); + + it("trims whitespace from account ID", () => { + expect(stellarAccountUrl("testnet", ` ${STELLAR_ACCOUNT_ID} `)).toBe( + `https://stellar.expert/explorer/testnet/account/${STELLAR_ACCOUNT_ID}`, + ); + }); + + it("returns null for an unrecognised network", () => { + expect(stellarAccountUrl("unknown" as any, STELLAR_ACCOUNT_ID)).toBeNull(); + }); + + it("returns null for empty or invalid account ID inputs", () => { + expect(stellarAccountUrl("testnet", "")).toBeNull(); + expect(stellarAccountUrl("testnet", " ")).toBeNull(); + expect(stellarAccountUrl("testnet", null as any)).toBeNull(); + expect(stellarAccountUrl("testnet", undefined as any)).toBeNull(); + }); }); describe("stellarContractUrl", () => { - it("builds a testnet contract URL", () => { - expect(stellarContractUrl("testnet", CONTRACT_ID)).toBe( - `https://stellar.expert/explorer/testnet/contract/${CONTRACT_ID}`, + it("builds a testnet Soroban contract URL", () => { + expect(stellarContractUrl("testnet", SOROBAN_CONTRACT_ID)).toBe( + `https://stellar.expert/explorer/testnet/contract/${SOROBAN_CONTRACT_ID}`, ); }); - it("builds a public (mainnet) contract URL", () => { - expect(stellarContractUrl("public", CONTRACT_ID)).toBe( - `https://stellar.expert/explorer/public/contract/${CONTRACT_ID}`, + it("builds a public (mainnet) Soroban contract URL placeholder", () => { + expect(stellarContractUrl("public", SOROBAN_CONTRACT_ID)).toBe( + `https://stellar.expert/explorer/public/contract/${SOROBAN_CONTRACT_ID}`, + ); + }); + + it("trims whitespace from contract ID", () => { + expect(stellarContractUrl("testnet", ` ${SOROBAN_CONTRACT_ID} `)).toBe( + `https://stellar.expert/explorer/testnet/contract/${SOROBAN_CONTRACT_ID}`, ); }); it("returns null for an unrecognised network", () => { - expect(stellarContractUrl("unknown" as any, CONTRACT_ID)).toBeNull(); + expect(stellarContractUrl("unknown" as any, SOROBAN_CONTRACT_ID)).toBeNull(); + }); + + it("returns null for empty or invalid contract ID inputs", () => { + expect(stellarContractUrl("testnet", "")).toBeNull(); + expect(stellarContractUrl("testnet", " ")).toBeNull(); + expect(stellarContractUrl("testnet", null as any)).toBeNull(); + expect(stellarContractUrl("testnet", undefined as any)).toBeNull(); }); }); });