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
4 changes: 4 additions & 0 deletions packages/sdk/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand Down
61 changes: 45 additions & 16 deletions packages/sdk/src/explorers/index.ts
Original file line number Diff line number Diff line change
@@ -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`.
// ---------------------------------------------------------------------------

// ---------------------------------------------------------------
Expand All @@ -30,6 +31,14 @@ const STELLAR_BASE_URLS: Record<StellarNetwork, string> = {
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
// ---------------------------------------------------------------
Expand All @@ -40,16 +49,16 @@ const STELLAR_BASE_URLS: Record<StellarNetwork, string> = {
* @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()}`;
}

/**
Expand All @@ -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()}`;
}
107 changes: 96 additions & 11 deletions packages/sdk/test/explorers.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,19 @@ import {
ethereumTxUrl,
ethereumAddressUrl,
stellarTxUrl,
stellarAccountUrl,
stellarContractUrl,
} from "../src/explorers/index.js";

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", () => {
Expand All @@ -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", () => {
Expand All @@ -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", () => {
Expand All @@ -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();
});
});
});