@lifi/composer-sdk
Version:
Public Composer SDK for building and submitting flows
123 lines (111 loc) • 4.46 kB
text/typescript
import type { ChainSummary } from '../discovery.js';
import type { ComposeSdk } from '../sdk.js';
/**
* The slice of {@link ComposeSdk} these helpers need. Depending on the one
* capability rather than the whole SDK keeps them testable with a two-line
* fake, and documents that nothing here touches the compile path.
*/
export type SupportedChainsSource = Pick<ComposeSdk, 'getSupportedChains'>;
/**
* A memoised view of the supported-chain list.
*/
export interface SupportedChainsCache {
/** Supported chains, ascending by `chainId`. Fetched once, then served from memory. */
readonly get: () => Promise<readonly ChainSummary[]>;
/**
* Resolves when `chainId` is supported, rejects with an actionable message
* naming the alternatives when it is not.
*/
readonly assertSupported: (chainId: number) => Promise<void>;
/**
* Display label for `chainId`, or `undefined` when the deployment does not
* support it. Served from the same cached fetch as {@link get}.
*/
readonly nameOf: (chainId: number) => Promise<string | undefined>;
/** Drops the memoised list so the next {@link get} refetches. */
readonly refresh: () => void;
}
/**
* Wraps `sdk.getSupportedChains()` in a cache so a long-lived process pays for
* one round trip instead of one per check.
*
* The SDK deliberately does not cache the list — the backend's chain registry
* refreshes periodically, so the right lifetime is the caller's to choose.
* Chains are added rarely, so caching for the life of a request handler (or
* refreshing on a timer via {@link SupportedChainsCache.refresh}) is usually
* right.
*
* Demonstrates:
* - `sdk.getSupportedChains()`, the convenience accessor over `GET /chains`
* - Preflighting a chain so an unsupported one fails locally with a message
* naming the alternatives, instead of costing a server round trip
* - Turning the per-chain `name` into display labels without a second lookup
* or a hardcoded chain-name table
* - Memoising the in-flight promise, not just the settled value, so concurrent
* first callers share one request
*/
export const createSupportedChainsCache = (
sdk: SupportedChainsSource,
): SupportedChainsCache => {
let inFlight: Promise<readonly ChainSummary[]> | undefined;
const get = (): Promise<readonly ChainSummary[]> => {
// A rejected fetch must not be memoised, or one transient network blip
// would poison every later lookup.
inFlight ??= sdk.getSupportedChains().catch((err: unknown) => {
inFlight = undefined;
throw err;
});
return inFlight;
};
const find = async (chainId: number): Promise<ChainSummary | undefined> =>
(await get()).find((chain) => chain.chainId === chainId);
return {
get,
assertSupported: async (chainId: number): Promise<void> => {
if (await find(chainId)) return;
const chains = await get();
throw new Error(
`Chain ${chainId} is not supported by this Compose deployment. ` +
`Supported chains: ${chains
.map((chain) => `${chain.name} (${chain.chainId})`)
.join(', ')}`,
);
},
nameOf: async (chainId: number): Promise<string | undefined> =>
(await find(chainId))?.name,
refresh: (): void => {
inFlight = undefined;
},
};
};
/** Ethereum mainnet — supported by every deployment. */
const MAINNET = 1;
/** Not an EVM chain ID any deployment serves; stands in for a bad input. */
const UNSUPPORTED = 999_999;
/**
* Harness entry point: prints the live chain list with names, then shows the
* guard accepting a supported chain and rejecting an unsupported one.
*/
export const runSupportedChainsExample = async (
sdk: SupportedChainsSource,
): Promise<void> => {
const chains = createSupportedChainsCache(sdk);
const supported = await chains.get();
console.log(`Supported chains (${supported.length}):`);
for (const { chainId, name } of supported) {
console.log(` ${chainId}\t${name}`);
}
await chains.assertSupported(MAINNET);
// Served from cache: no second round trip.
console.log(
`\n${await chains.nameOf(MAINNET)} (${MAINNET}) is supported — safe to build a request.`,
);
await chains
.assertSupported(UNSUPPORTED)
.then(() => {
throw new Error(`expected chain ${UNSUPPORTED} to be rejected`);
})
.catch((err: unknown) => {
console.log(`\nGuard rejected as expected: ${(err as Error).message}`);
});
};