UNPKG

@ensnode/ensnode-sdk

Version:

A utility library for interacting with ENSNode and ENS data

1,424 lines (1,371 loc) 149 kB
// src/ensapi/config/deserialize.ts import { prettifyError as prettifyError2, ZodError } from "zod/v4"; // src/ensapi/config/zod-schemas.ts import { z as z3 } from "zod/v4"; // src/ensindexer/config/zod-schemas.ts import z2 from "zod/v4"; // src/shared/account-id.ts import { isAddressEqual } from "viem"; var accountIdEqual = (a, b) => { return a.chainId === b.chainId && isAddressEqual(a.address, b.address); }; // src/shared/address.ts function asLowerCaseAddress(address) { return address.toLowerCase(); } // src/shared/cache/lru-cache.ts var LruCache = class { _cache = /* @__PURE__ */ new Map(); _capacity; /** * Create a new LRU cache with the given capacity. * * @param capacity The maximum number of items in the cache. If set to 0, the cache is effectively disabled. * @throws Error if capacity is not a non-negative integer. */ constructor(capacity) { if (!Number.isInteger(capacity)) { throw new Error( `LruCache requires capacity to be an integer but a capacity of ${capacity} was requested.` ); } if (capacity < 0) { throw new Error( `LruCache requires a non-negative capacity but a capacity of ${capacity} was requested.` ); } this._capacity = capacity; } set(key, value) { this._cache.set(key, value); if (this._cache.size > this._capacity) { const oldestKey = this._cache.keys().next().value; this._cache.delete(oldestKey); } } get(key) { const value = this._cache.get(key); if (value) { this._cache.delete(key); this._cache.set(key, value); } return value; } clear() { this._cache.clear(); } get size() { return this._cache.size; } get capacity() { return this._capacity; } }; // src/shared/cache/swr-cache.ts import { secondsToMilliseconds } from "date-fns"; import { getUnixTime } from "date-fns/getUnixTime"; // src/shared/deserialize.ts import { prettifyError } from "zod/v4"; // src/shared/zod-schemas.ts import { AccountId as CaipAccountId } from "caip"; import { isAddress as isAddress2, isHex as isHex2, size } from "viem"; import z from "zod/v4"; // src/ens/index.ts import { getENSRootChainId } from "@ensnode/datasources"; // src/ens/coin-type.ts import { coinTypeToEvmChainId as _coinTypeToEvmChainId, evmChainIdToCoinType as _evmChainIdToCoinType } from "@ensdomains/address-encoder/utils"; var ETH_COIN_TYPE = 60; var DEFAULT_EVM_CHAIN_ID = 0; var DEFAULT_EVM_COIN_TYPE = 2147483648; var coinTypeToEvmChainId = (coinType) => { if (coinType === ETH_COIN_TYPE) return 1; return _coinTypeToEvmChainId(coinType); }; var evmChainIdToCoinType = (chainId) => { if (chainId === 1) return ETH_COIN_TYPE; return _evmChainIdToCoinType(chainId); }; var bigintToCoinType = (value) => { if (value > BigInt(Number.MAX_SAFE_INTEGER)) { throw new Error(`'${value}' cannot represent as CoinType, it is too large.`); } return Number(value); }; // src/ens/constants.ts import { namehash } from "viem"; var ROOT_NODE = namehash(""); var ETH_NODE = namehash("eth"); var BASENAMES_NODE = namehash("base.eth"); var LINEANAMES_NODE = namehash("linea.eth"); var ADDR_REVERSE_NODE = namehash("addr.reverse"); // src/ens/dns-encoded-name.ts import { bytesToString, hexToBytes } from "viem"; function decodeDNSEncodedLiteralName(packet) { return decodeDNSEncodedName(packet); } function decodeDNSEncodedName(packet) { const segments = []; const bytes = hexToBytes(packet); if (bytes.length === 0) throw new Error(`Packet is empty.`); let offset = 0; while (offset < bytes.length) { const len = bytes[offset]; if (len === void 0) { throw new Error(`Invariant: bytes[offset] is undefined after offset < bytes.length check.`); } if (len < 0 || len > 255) { throw new Error( `Invariant: this should be literally impossible, but an unsigned byte was less than zero or greater than 255. The value in question is ${len}` ); } if (len === 0) break; const segment = bytesToString(bytes.subarray(offset + 1, offset + len + 1)); segments.push(segment); offset += len + 1; } if (offset >= bytes.length) throw new Error(`Overflow, offset >= bytes.length`); if (offset !== bytes.length - 1) throw new Error(`Junk at end of name`); return segments; } // src/ens/labelhash.ts import { isHex } from "viem"; function isLabelHash(maybeLabelHash) { const expectedLength = maybeLabelHash.length === 66; const expectedEncoding = isHex(maybeLabelHash); const expectedCasing = maybeLabelHash === maybeLabelHash.toLowerCase(); return expectedLength && expectedEncoding && expectedCasing; } // src/ens/encode-labelhash.ts var encodeLabelHash = (labelHash) => `[${labelHash.slice(2)}]`; function isEncodedLabelHash(maybeEncodedLabelHash) { const expectedFormatting = maybeEncodedLabelHash.startsWith("[") && maybeEncodedLabelHash.endsWith("]"); const includesLabelHash = isLabelHash(`0x${maybeEncodedLabelHash.slice(1, -1)}`); return expectedFormatting && includesLabelHash; } // src/ens/is-normalized.ts import { normalize } from "viem/ens"; function isNormalizedName(name) { try { return name === normalize(name); } catch { return false; } } function isNormalizedLabel(label) { if (label === "") return false; if (label.includes(".")) return false; try { return label === normalize(label); } catch { return false; } } // src/ens/names.ts import { ens_beautify } from "@adraffy/ens-normalize"; var ENS_ROOT = ""; var getNameHierarchy = (name) => name.split(".").map((_, i, labels) => labels.slice(i).join(".")); var getParentNameFQDN = (name) => { if (name === ENS_ROOT) { throw new Error("There is no parent name for ENS Root."); } const labels = name.split("."); if (labels.length === 1) { return ENS_ROOT; } return labels.slice(1).join("."); }; var beautifyName = (name) => { const beautifiedLabels = name.split(".").map((label) => { if (isNormalizedLabel(label)) { return ens_beautify(label); } else { return label; } }); return beautifiedLabels.join("."); }; // src/ens/parse-reverse-name.ts import { hexToBigInt, isAddress } from "viem"; var REVERSE_NAME_REGEX = /^([0-9a-fA-F]+)\.([0-9a-f]{1,64}|addr|default)\.reverse$/; var parseAddressLabel = (addressLabel) => { const maybeAddress = `0x${addressLabel}`; if (!isAddress(maybeAddress)) { throw new Error(`Invalid EVM address "${maybeAddress}"`); } return asLowerCaseAddress(maybeAddress); }; var parseCoinTypeLabel = (coinTypeLabel) => { if (coinTypeLabel === "default") return DEFAULT_EVM_COIN_TYPE; if (coinTypeLabel === "addr") return ETH_COIN_TYPE; return bigintToCoinType(hexToBigInt(`0x${coinTypeLabel}`)); }; function parseReverseName(name) { const match = name.match(REVERSE_NAME_REGEX); if (!match) return null; try { const [, addressLabel, coinTypeLabel] = match; if (!addressLabel) return null; if (!coinTypeLabel) return null; return { address: parseAddressLabel(addressLabel), coinType: parseCoinTypeLabel(coinTypeLabel) }; } catch { return null; } } // src/ens/reverse-name.ts var addrReverseLabel = (address) => address.slice(2); var coinTypeReverseLabel = (coinType) => coinType.toString(16); function reverseName(address, coinType) { const label = addrReverseLabel(address); const middle = (() => { switch (coinType) { case ETH_COIN_TYPE: return "addr"; case DEFAULT_EVM_COIN_TYPE: return "default"; default: return coinTypeReverseLabel(coinType); } })(); return `${label}.${middle}.reverse`; } // src/ens/subname-helpers.ts import { concat, keccak256, toHex } from "viem"; var makeSubdomainNode = (labelHash, node) => keccak256(concat([node, labelHash])); var uint256ToHex32 = (num) => toHex(num, { size: 32 }); // src/ens/types.ts import { ENSNamespaceIds } from "@ensnode/datasources"; // src/shared/currencies.ts var CurrencyIds = { ETH: "ETH", USDC: "USDC", DAI: "DAI" }; var currencyInfo = { [CurrencyIds.ETH]: { id: CurrencyIds.ETH, name: "ETH", decimals: 18 }, [CurrencyIds.USDC]: { id: CurrencyIds.USDC, name: "USDC", decimals: 6 }, [CurrencyIds.DAI]: { id: CurrencyIds.DAI, name: "Dai Stablecoin", decimals: 18 } }; function getCurrencyInfo(currencyId) { return currencyInfo[currencyId]; } function priceEth(amount) { return { amount, currency: CurrencyIds.ETH }; } function priceUsdc(amount) { return { amount, currency: CurrencyIds.USDC }; } function priceDai(amount) { return { amount, currency: CurrencyIds.DAI }; } function isPriceCurrencyEqual(priceA, priceB) { return priceA.currency === priceB.currency; } function isPriceEqual(priceA, priceB) { return isPriceCurrencyEqual(priceA, priceB) && priceA.amount === priceB.amount; } function addPrices(...prices) { const firstPrice = prices[0]; const allPricesInSameCurrency = prices.every((price) => isPriceCurrencyEqual(firstPrice, price)); if (allPricesInSameCurrency === false) { throw new Error("All prices must have the same currency to be added together."); } const { currency } = firstPrice; return prices.reduce( (acc, price) => ({ amount: acc.amount + price.amount, currency }), { amount: 0n, currency: firstPrice.currency } ); } // src/shared/reinterpretation.ts import { labelhash as labelToLabelHash } from "viem"; function reinterpretLabel(label) { if (label === "") { throw new Error( `Cannot reinterpret an empty label that violates the invariants of an InterpretedLabel.` ); } if (isEncodedLabelHash(label)) return label; if (isNormalizedLabel(label)) return label; return encodeLabelHash(labelToLabelHash(label)); } function reinterpretName(name) { if (name === "") return name; const interpretedLabels = name.split("."); const reinterpretedLabels = interpretedLabels.map(reinterpretLabel); const reinterpretedName = reinterpretedLabels.join("."); return reinterpretedName; } // src/shared/zod-schemas.ts var makeFiniteNonNegativeNumberSchema = (valueLabel = "Value") => z.number({ // NOTE: Zod's implementation of `number` automatically rejects NaN and Infinity values. // and therefore the finite check is implicit. error: `${valueLabel} must be a finite number.` }).nonnegative({ error: `${valueLabel} must be a non-negative number (>=0).` }); var makeIntegerSchema = (valueLabel = "Value") => z.int({ error: `${valueLabel} must be an integer.` }); var makePositiveIntegerSchema = (valueLabel = "Value") => makeIntegerSchema(valueLabel).positive({ error: `${valueLabel} must be a positive integer (>0).` }); var makeNonNegativeIntegerSchema = (valueLabel = "Value") => makeIntegerSchema(valueLabel).nonnegative({ error: `${valueLabel} must be a non-negative integer (>=0).` }); var makeDurationSchema = (valueLabel = "Value") => z.coerce.number({ error: `${valueLabel} must be a number.` }).pipe(makeNonNegativeIntegerSchema(valueLabel)); var makeChainIdSchema = (valueLabel = "Chain ID") => makePositiveIntegerSchema(valueLabel).transform((val) => val); var makeChainIdStringSchema = (valueLabel = "Chain ID String") => z.string({ error: `${valueLabel} must be a string representing a chain ID.` }).pipe(z.coerce.number({ error: `${valueLabel} must represent a positive integer (>0).` })).pipe(makeChainIdSchema(`The numeric value represented by ${valueLabel}`)); var makeLowercaseAddressSchema = (valueLabel = "EVM address") => z.string().check((ctx) => { if (!isAddress2(ctx.value)) { ctx.issues.push({ code: "custom", message: `${valueLabel} must be a valid EVM address`, input: ctx.value }); } }).transform((val) => asLowerCaseAddress(val)); var makeDatetimeSchema = (valueLabel = "Datetime string") => z.iso.datetime({ error: `${valueLabel} must be a string in ISO 8601 format.` }).transform((v) => new Date(v)); var makeUnixTimestampSchema = (valueLabel = "Timestamp") => makeIntegerSchema(valueLabel); var makeUrlSchema = (valueLabel = "Value") => z.url({ error: `${valueLabel} must be a valid URL string (e.g., http://localhost:8080 or https://example.com).`, abort: true }).transform((v) => new URL(v)); var makeBlockNumberSchema = (valueLabel = "Block number") => makeNonNegativeIntegerSchema(valueLabel); var makeBlockrangeSchema = (valueLabel = "Value") => z.strictObject( { startBlock: makeBlockNumberSchema(`${valueLabel}.startBlock`).optional(), endBlock: makeBlockNumberSchema(`${valueLabel}.endBlock`).optional() }, { error: `${valueLabel} must be a valid Blockrange object.` } ).refine( (v) => { if (v.startBlock && v.endBlock) { return v.startBlock <= v.endBlock; } return true; }, { error: `${valueLabel}: startBlock must be before or equal to endBlock` } ); var makeBlockRefSchema = (valueLabel = "Value") => z.strictObject( { timestamp: makeUnixTimestampSchema(`${valueLabel}.timestamp`), number: makeBlockNumberSchema(`${valueLabel}.number`) }, { error: `${valueLabel} must be a valid BlockRef object.` } ); var makeENSNamespaceIdSchema = (valueLabel = "ENSNamespaceId") => z.enum(ENSNamespaceIds, { error() { return `Invalid ${valueLabel}. Supported ENS namespace IDs are: ${Object.keys(ENSNamespaceIds).join(", ")}`; } }); var makePriceAmountSchema = (valueLabel = "Amount") => z.coerce.bigint({ error: `${valueLabel} must represent a bigint.` }).nonnegative({ error: `${valueLabel} must not be negative.` }); var makePriceCurrencySchema = (currency, valueLabel = "Price Currency") => z.strictObject({ amount: makePriceAmountSchema(`${valueLabel} amount`), currency: z.literal(currency, { error: `${valueLabel} currency must be set to '${currency}'.` }) }); var makePriceEthSchema = (valueLabel = "Price ETH") => makePriceCurrencySchema(CurrencyIds.ETH, valueLabel).transform((v) => v); var makeAccountIdSchema = (valueLabel = "AccountId") => z.strictObject({ chainId: makeChainIdSchema(`${valueLabel} chain ID`), address: makeLowercaseAddressSchema(`${valueLabel} address`) }); var makeAccountIdStringSchema = (valueLabel = "Account ID String") => z.coerce.string().transform((v) => { const result = new CaipAccountId(v); return { chainId: Number(result.chainId.reference), address: result.address }; }).pipe(makeAccountIdSchema(valueLabel)); var makeHexStringSchema = (options, valueLabel = "String representation of bytes array") => z.string().check(function invariant_isHexEncoded(ctx) { if (!isHex2(ctx.value)) { ctx.issues.push({ code: "custom", input: ctx.value, message: `${valueLabel} must be a hexadecimal value which starts with '0x'.` }); } }).transform((v) => v).check(function invariant_encodesRequiredBytesCount(ctx) { const expectedBytesCount = options.bytesCount; const actualBytesCount = size(ctx.value); if (actualBytesCount !== expectedBytesCount) { ctx.issues.push({ code: "custom", input: ctx.value, message: `${valueLabel} must represent exactly ${expectedBytesCount} bytes. Currently represented bytes count: ${actualBytesCount}.` }); } }); var makeNodeSchema = (valueLabel = "Node") => makeHexStringSchema({ bytesCount: 32 }, valueLabel); var makeTransactionHashSchema = (valueLabel = "Transaction hash") => makeHexStringSchema({ bytesCount: 32 }, valueLabel); var makeReinterpretedNameSchema = (valueLabel = "Reinterpreted Name") => z.string().transform((v) => v).check((ctx) => { try { reinterpretName(ctx.value); } catch (error) { const errorMessage = error instanceof Error ? error.message : "Unknown error"; ctx.issues.push({ code: "custom", input: ctx.value, message: `${valueLabel} cannot be reinterpreted: ${errorMessage}` }); } }).transform(reinterpretName); // src/shared/deserialize.ts function deserializeChainId(maybeChainId, valueLabel) { const schema = makeChainIdStringSchema(valueLabel); const parsed = schema.safeParse(maybeChainId); if (parsed.error) { throw new Error(`Cannot deserialize ChainId: ${prettifyError(parsed.error)} `); } return parsed.data; } function deserializeDatetime(maybeDatetime, valueLabel) { const schema = makeDatetimeSchema(valueLabel); const parsed = schema.safeParse(maybeDatetime); if (parsed.error) { throw new Error(`Cannot deserialize Datetime: ${prettifyError(parsed.error)} `); } return parsed.data; } function deserializeUnixTimestamp(maybeTimestamp, valueLabel) { const schema = makeUnixTimestampSchema(valueLabel); const parsed = schema.safeParse(maybeTimestamp); if (parsed.error) { throw new Error(`Cannot deserialize Unix Timestamp: ${prettifyError(parsed.error)} `); } return parsed.data; } function deserializeUrl(maybeUrl, valueLabel) { const schema = makeUrlSchema(valueLabel); const parsed = schema.safeParse(maybeUrl); if (parsed.error) { throw new Error(`Cannot deserialize URL: ${prettifyError(parsed.error)} `); } return parsed.data; } function deserializeBlockNumber(maybeBlockNumber, valueLabel) { const schema = makeBlockNumberSchema(valueLabel); const parsed = schema.safeParse(maybeBlockNumber); if (parsed.error) { throw new Error(`Cannot deserialize BlockNumber: ${prettifyError(parsed.error)} `); } return parsed.data; } function deserializeBlockrange(maybeBlockrange, valueLabel) { const schema = makeBlockrangeSchema(valueLabel); const parsed = schema.safeParse(maybeBlockrange); if (parsed.error) { throw new Error(`Cannot deserialize Blockrange: ${prettifyError(parsed.error)} `); } return parsed.data; } function deserializeBlockRef(maybeBlockRef, valueLabel) { const schema = makeBlockRefSchema(valueLabel); const parsed = schema.safeParse(maybeBlockRef); if (parsed.error) { throw new Error(`Cannot deserialize BlockRef: ${prettifyError(parsed.error)} `); } return parsed.data; } function deserializeDuration(maybeDuration, valueLabel) { const schema = makeDurationSchema(valueLabel); const parsed = schema.safeParse(maybeDuration); if (parsed.error) { throw new RangeError(`Cannot deserialize Duration: ${prettifyError(parsed.error)} `); } return parsed.data; } function parseAccountId(maybeAccountId, valueLabel) { const schema = makeAccountIdStringSchema(valueLabel); const parsed = schema.safeParse(maybeAccountId); if (parsed.error) { throw new RangeError(`Cannot deserialize AccountId: ${prettifyError(parsed.error)} `); } return parsed.data; } // src/shared/datetime.ts function durationBetween(start, end) { return deserializeDuration(end - start, "Duration"); } function addDuration(timestamp, duration) { return deserializeUnixTimestamp(timestamp + duration, "UnixTimestamp"); } // src/shared/cache/swr-cache.ts var SWRCache = class { constructor(options) { this.options = options; if (options.proactiveRevalidationInterval) { this.backgroundInterval = setInterval( () => this.revalidate(), secondsToMilliseconds(options.proactiveRevalidationInterval) ); } if (options.proactivelyInitialize) this.revalidate(); } cache = null; inProgressRevalidate = null; backgroundInterval = null; async revalidate() { if (!this.inProgressRevalidate) { this.inProgressRevalidate = this.options.fn().then((result) => { this.cache = { result, updatedAt: getUnixTime(/* @__PURE__ */ new Date()) }; }).catch((error) => { if (!this.cache) { this.cache = { // ensure thrown value is always an Error instance result: error instanceof Error ? error : new Error(String(error)), updatedAt: getUnixTime(/* @__PURE__ */ new Date()) }; } }).finally(() => { this.inProgressRevalidate = null; }); } return this.inProgressRevalidate; } /** * Read the most recently cached result from the `SWRCache`. * * @returns a `ValueType` that was most recently successfully returned by `fn` or `Error` if `fn` * has never successfully returned. */ async read() { if (!this.cache) await this.revalidate(); if (!this.cache) throw new Error("never"); if (durationBetween(this.cache.updatedAt, getUnixTime(/* @__PURE__ */ new Date())) > this.options.ttl) { this.revalidate(); } return this.cache.result; } /** * Destroys the background revalidation interval, if exists. */ destroy() { if (this.backgroundInterval) { clearInterval(this.backgroundInterval); this.backgroundInterval = null; } } }; // src/shared/cache/ttl-cache.ts import { getUnixTime as getUnixTime2 } from "date-fns/getUnixTime"; var TtlCache = class { _cache = /* @__PURE__ */ new Map(); _ttl; /** * Create a new TTL cache with the given TTL. * * @param ttl Time-to-live duration in seconds. Items expire after this duration. */ constructor(ttl) { this._ttl = ttl; } _cleanup() { const now = getUnixTime2(/* @__PURE__ */ new Date()); for (const [key, entry] of this._cache.entries()) { if (entry.expiresAt <= now) { this._cache.delete(key); } } } set(key, value) { this._cleanup(); const expiresAt = addDuration(getUnixTime2(/* @__PURE__ */ new Date()), this._ttl); this._cache.set(key, { value, expiresAt }); } get(key) { this._cleanup(); const entry = this._cache.get(key); if (!entry) { return void 0; } if (entry.expiresAt <= getUnixTime2(/* @__PURE__ */ new Date())) { this._cache.delete(key); return void 0; } return entry.value; } clear() { this._cache.clear(); } get size() { this._cleanup(); return this._cache.size; } get capacity() { return Number.MAX_SAFE_INTEGER; } has(key) { this._cleanup(); const entry = this._cache.get(key); if (!entry) { return false; } if (entry.expiresAt <= getUnixTime2(/* @__PURE__ */ new Date())) { this._cache.delete(key); return false; } return true; } delete(key) { return this._cache.delete(key); } }; // src/shared/collections.ts var uniq = (arr) => [...new Set(arr)]; // src/shared/datasource-contract.ts import { maybeGetDatasource } from "@ensnode/datasources"; var maybeGetDatasourceContract = (namespaceId, datasourceName, contractName) => { const datasource = maybeGetDatasource(namespaceId, datasourceName); if (!datasource) return void 0; const address = datasource.contracts[contractName]?.address; if (address === void 0 || Array.isArray(address)) return void 0; return { chainId: datasource.chain.id, address }; }; var getDatasourceContract = (namespaceId, datasourceName, contractName) => { const contract = maybeGetDatasourceContract(namespaceId, datasourceName, contractName); if (!contract) { throw new Error( `Expected contract not found for ${namespaceId} ${datasourceName} ${contractName}` ); } return contract; }; // src/shared/labelhash.ts import { keccak256 as keccak2562, stringToBytes } from "viem"; var labelhashLiteralLabel = (label) => keccak2562(stringToBytes(label)); // src/shared/interpretation.ts function literalLabelToInterpretedLabel(label) { if (isNormalizedLabel(label)) return label; return encodeLabelHash(labelhashLiteralLabel(label)); } function literalLabelsToInterpretedName(labels) { return labels.map(literalLabelToInterpretedLabel).join("."); } function interpretedLabelsToInterpretedName(labels) { return labels.join("."); } function literalLabelsToLiteralName(labels) { return labels.join("."); } // src/shared/null-bytes.ts var hasNullByte = (value) => value.indexOf("\0") !== -1; var stripNullBytes = (value) => value.replaceAll("\0", ""); // src/shared/numbers.ts function bigIntToNumber(n) { if (n < Number.MIN_SAFE_INTEGER) { throw new Error( `The bigint '${n.toString()}' value is too low to be to converted into a number.'` ); } if (n > Number.MAX_SAFE_INTEGER) { throw new Error( `The bigint '${n.toString()}' value is too high to be to converted into a number.'` ); } return Number(n); } // src/shared/serialize.ts import { AccountId as CaipAccountId2 } from "caip"; function serializeChainId(chainId) { return chainId.toString(); } function serializeDatetime(datetime) { return datetime.toISOString(); } function serializeUrl(url) { return url.toString(); } function serializePrice(price) { return { currency: price.currency, amount: price.amount.toString() }; } function serializePriceEth(price) { return serializePrice(price); } function formatAccountId(accountId) { return CaipAccountId2.format({ chainId: { namespace: "eip155", reference: accountId.chainId.toString() }, address: accountId.address }).toLowerCase(); } // src/shared/url.ts function isHttpProtocol(url) { return ["http:", "https:"].includes(url.protocol); } function isWebSocketProtocol(url) { return ["ws:", "wss:"].includes(url.protocol); } // src/ensindexer/config/is-subgraph-compatible.ts import { ENSNamespaceIds as ENSNamespaceIds2 } from "@ensnode/datasources"; // src/ensindexer/config/types.ts var PluginName = /* @__PURE__ */ ((PluginName2) => { PluginName2["Subgraph"] = "subgraph"; PluginName2["Basenames"] = "basenames"; PluginName2["Lineanames"] = "lineanames"; PluginName2["ThreeDNS"] = "threedns"; PluginName2["ProtocolAcceleration"] = "protocol-acceleration"; PluginName2["Registrars"] = "registrars"; PluginName2["TokenScope"] = "tokenscope"; return PluginName2; })(PluginName || {}); // src/ensindexer/config/is-subgraph-compatible.ts function isSubgraphCompatible(config) { const onlySubgraphPluginActivated = config.plugins.length === 1 && config.plugins[0] === "subgraph" /* Subgraph */; const isSubgraphLabelSet = config.labelSet.labelSetId === "subgraph" && config.labelSet.labelSetVersion === 0; const isEnsTestEnvLabelSet = config.labelSet.labelSetId === "ens-test-env" && config.labelSet.labelSetVersion === 0; const labelSetIsSubgraphCompatible = isSubgraphLabelSet || config.namespace === ENSNamespaceIds2.EnsTestEnv && isEnsTestEnvLabelSet; return onlySubgraphPluginActivated && labelSetIsSubgraphCompatible; } // src/ensindexer/config/validations.ts function invariant_ensDbVersionIsSameAsEnsIndexerVersion(ctx) { const versionInfo = ctx.value; if (versionInfo.ensDb !== versionInfo.ensIndexer) { ctx.issues.push({ code: "custom", input: versionInfo, message: "`ensDb` version must be same as `ensIndexer` version" }); } } // src/ensindexer/config/zod-schemas.ts var makeIndexedChainIdsSchema = (valueLabel = "Indexed Chain IDs") => z2.array(makeChainIdSchema(valueLabel), { error: `${valueLabel} must be an array.` }).min(1, { error: `${valueLabel} list must include at least one element.` }).transform((v) => new Set(v)); var makePluginsListSchema = (valueLabel = "Plugins") => z2.array(z2.string(), { error: `${valueLabel} must be a list of strings.` }).min(1, { error: `${valueLabel} must be a list of strings with at least one string value` }).refine((arr) => arr.length === uniq(arr).length, { error: `${valueLabel} cannot contain duplicate values.` }); var makeDatabaseSchemaNameSchema = (valueLabel = "Database schema name") => z2.string({ error: `${valueLabel} must be a string` }).trim().nonempty({ error: `${valueLabel} is required and must be a non-empty string.` }); var makeLabelSetIdSchema = (valueLabel) => { return z2.string({ error: `${valueLabel} must be a string` }).min(1, { error: `${valueLabel} must be 1-50 characters long` }).max(50, { error: `${valueLabel} must be 1-50 characters long` }).regex(/^[a-z-]+$/, { error: `${valueLabel} can only contain lowercase letters (a-z) and hyphens (-)` }); }; var makeLabelSetVersionSchema = (valueLabel) => { return z2.coerce.number({ error: `${valueLabel} must be an integer.` }).pipe(makeNonNegativeIntegerSchema(valueLabel)); }; var makeFullyPinnedLabelSetSchema = (valueLabel = "Label set") => { let valueLabelLabelSetId = valueLabel; let valueLabelLabelSetVersion = valueLabel; if (valueLabel === "LABEL_SET") { valueLabelLabelSetId = "LABEL_SET_ID"; valueLabelLabelSetVersion = "LABEL_SET_VERSION"; } else { valueLabelLabelSetId = `${valueLabel}.labelSetId`; valueLabelLabelSetVersion = `${valueLabel}.labelSetVersion`; } return z2.object({ labelSetId: makeLabelSetIdSchema(valueLabelLabelSetId), labelSetVersion: makeLabelSetVersionSchema(valueLabelLabelSetVersion) }); }; var makeNonEmptyStringSchema = (valueLabel = "Value") => z2.string().nonempty({ error: `${valueLabel} must be a non-empty string.` }); var makeENSIndexerVersionInfoSchema = (valueLabel = "Value") => z2.strictObject( { nodejs: makeNonEmptyStringSchema(), ponder: makeNonEmptyStringSchema(), ensDb: makeNonEmptyStringSchema(), ensIndexer: makeNonEmptyStringSchema(), ensNormalize: makeNonEmptyStringSchema(), ensRainbow: makeNonEmptyStringSchema(), ensRainbowSchema: makePositiveIntegerSchema() }, { error: `${valueLabel} must be a valid ENSIndexerVersionInfo object.` } ).check(invariant_ensDbVersionIsSameAsEnsIndexerVersion); function invariant_isSubgraphCompatibleRequirements(ctx) { const { value: config } = ctx; if (config.isSubgraphCompatible && !isSubgraphCompatible(config)) { ctx.issues.push({ code: "custom", input: config, message: `'isSubgraphCompatible' requires only the '${"subgraph" /* Subgraph */}' plugin to be active and labelSet must be {labelSetId: "subgraph", labelSetVersion: 0}` }); } } var makeENSIndexerPublicConfigSchema = (valueLabel = "ENSIndexerPublicConfig") => z2.object({ labelSet: makeFullyPinnedLabelSetSchema(`${valueLabel}.labelSet`), indexedChainIds: makeIndexedChainIdsSchema(`${valueLabel}.indexedChainIds`), isSubgraphCompatible: z2.boolean({ error: `${valueLabel}.isSubgraphCompatible` }), namespace: makeENSNamespaceIdSchema(`${valueLabel}.namespace`), plugins: makePluginsListSchema(`${valueLabel}.plugins`), databaseSchemaName: makeDatabaseSchemaNameSchema(`${valueLabel}.databaseSchemaName`), versionInfo: makeENSIndexerVersionInfoSchema(`${valueLabel}.versionInfo`) }).check(invariant_isSubgraphCompatibleRequirements); // src/ensapi/config/zod-schemas.ts var TheGraphCannotFallbackReasonSchema = z3.enum({ NotSubgraphCompatible: "not-subgraph-compatible", NoApiKey: "no-api-key", NoSubgraphUrl: "no-subgraph-url" }); var TheGraphFallbackSchema = z3.strictObject({ canFallback: z3.boolean(), reason: TheGraphCannotFallbackReasonSchema.nullable() }); function makeENSApiPublicConfigSchema(valueLabel) { const label = valueLabel ?? "ENSApiPublicConfig"; return z3.strictObject({ version: z3.string().min(1, `${label}.version must be a non-empty string`), theGraphFallback: TheGraphFallbackSchema, ensIndexerPublicConfig: makeENSIndexerPublicConfigSchema(`${label}.ensIndexerPublicConfig`) }); } // src/ensapi/config/deserialize.ts function deserializeENSApiPublicConfig(maybeConfig, valueLabel) { const schema = makeENSApiPublicConfigSchema(valueLabel); try { return schema.parse(maybeConfig); } catch (error) { if (error instanceof ZodError) { throw new Error(`Cannot deserialize ENSApiPublicConfig: ${prettifyError2(error)} `); } throw error; } } // src/ensindexer/config/deserialize.ts import { prettifyError as prettifyError3 } from "zod/v4"; function deserializeENSIndexerPublicConfig(maybeConfig, valueLabel) { const schema = makeENSIndexerPublicConfigSchema(valueLabel); const parsed = schema.safeParse(maybeConfig); if (parsed.error) { throw new Error(`Cannot deserialize ENSIndexerPublicConfig: ${prettifyError3(parsed.error)} `); } return parsed.data; } // src/ensindexer/config/label-utils.ts import { hexToBytes as hexToBytes2 } from "viem"; function labelHashToBytes(labelHash) { try { if (labelHash.length !== 66) { throw new Error(`Invalid labelHash length ${labelHash.length} characters (expected 66)`); } if (labelHash !== labelHash.toLowerCase()) { throw new Error("Labelhash must be in lowercase"); } if (!labelHash.startsWith("0x")) { throw new Error("Labelhash must be 0x-prefixed"); } const bytes = hexToBytes2(labelHash); if (bytes.length !== 32) { throw new Error(`Invalid labelHash length ${bytes.length} bytes (expected 32)`); } return bytes; } catch (e) { if (e instanceof Error) { throw e; } throw new Error("Invalid hex format"); } } // src/ensindexer/config/labelset-utils.ts function buildLabelSetId(maybeLabelSetId) { return makeLabelSetIdSchema("LabelSetId").parse(maybeLabelSetId); } function buildLabelSetVersion(maybeLabelSetVersion) { return makeLabelSetVersionSchema("LabelSetVersion").parse(maybeLabelSetVersion); } function buildEnsRainbowClientLabelSet(labelSetId, labelSetVersion) { if (labelSetVersion !== void 0 && labelSetId === void 0) { throw new Error("When a labelSetVersion is defined, labelSetId must also be defined."); } return { labelSetId, labelSetVersion }; } function validateSupportedLabelSetAndVersion(serverSet, clientSet) { if (clientSet.labelSetId === void 0) { return; } if (serverSet.labelSetId !== clientSet.labelSetId) { throw new Error( `Server label set ID "${serverSet.labelSetId}" does not match client's requested label set ID "${clientSet.labelSetId}".` ); } if (clientSet.labelSetVersion !== void 0 && serverSet.highestLabelSetVersion < clientSet.labelSetVersion) { throw new Error( `Server highest label set version ${serverSet.highestLabelSetVersion} is less than client's requested version ${clientSet.labelSetVersion} for label set ID "${clientSet.labelSetId}".` ); } } // src/ensindexer/config/parsing.ts function parseNonNegativeInteger(maybeNumber) { const trimmed = maybeNumber.trim(); if (!trimmed) { throw new Error("Input cannot be empty"); } if (trimmed === "-0") { throw new Error("Negative zero is not a valid non-negative integer"); } const num = Number(maybeNumber); if (Number.isNaN(num)) { throw new Error(`"${maybeNumber}" is not a valid number`); } if (!Number.isFinite(num)) { throw new Error(`"${maybeNumber}" is not a finite number`); } if (!Number.isInteger(num)) { throw new Error(`"${maybeNumber}" is not an integer`); } if (num < 0) { throw new Error(`"${maybeNumber}" is not a non-negative integer`); } return num; } // src/ensindexer/config/serialize.ts function serializeIndexedChainIds(indexedChainIds) { return Array.from(indexedChainIds); } function serializeENSIndexerPublicConfig(config) { const { labelSet, indexedChainIds, databaseSchemaName, isSubgraphCompatible: isSubgraphCompatible2, namespace, plugins, versionInfo } = config; return { labelSet, indexedChainIds: serializeIndexedChainIds(indexedChainIds), databaseSchemaName, isSubgraphCompatible: isSubgraphCompatible2, namespace, plugins, versionInfo }; } // src/ensindexer/indexing-status/deserialize.ts import { prettifyError as prettifyError4 } from "zod/v4"; // src/ensindexer/indexing-status/zod-schemas.ts import z4 from "zod/v4"; // src/ensindexer/indexing-status/types.ts var ChainIndexingConfigTypeIds = { /** * Represents that indexing of the chain should be performed for an indefinite range. */ Indefinite: "indefinite", /** * Represents that indexing of the chain should be performed for a definite range. */ Definite: "definite" }; var ChainIndexingStatusIds = { /** * Represents that indexing of the chain is not ready to begin yet because: * - ENSIndexer is in its initialization phase and the data to build a * "true" {@link ChainIndexingSnapshot} for the chain is still being loaded; or * - ENSIndexer is using an omnichain indexing strategy and the * `omnichainIndexingCursor` is <= `config.startBlock.timestamp` for the chain's * {@link ChainIndexingSnapshot}. */ Queued: "chain-queued", /** * Represents that indexing of the chain is in progress and under a special * "backfill" phase that optimizes for accelerated indexing until reaching the * "fixed target" `backfillEndBlock`. */ Backfill: "chain-backfill", /** * Represents that the "backfill" phase of indexing the chain is completed * and that the chain is configured to be indexed for an indefinite range. * Therefore, indexing of the chain remains indefinitely in progress where * ENSIndexer will continuously work to discover and index new blocks as they * are added to the chain across time. */ Following: "chain-following", /** * Represents that indexing of the chain is completed as the chain is configured * to be indexed for a definite range and the indexing of all blocks through * that definite range is completed. */ Completed: "chain-completed" }; var OmnichainIndexingStatusIds = { /** * Represents that omnichain indexing is not ready to begin yet because * ENSIndexer is in its initialization phase and the data to build a "true" * {@link OmnichainIndexingStatusSnapshot} is still being loaded. */ Unstarted: "omnichain-unstarted", /** * Represents that omnichain indexing is in an overall "backfill" status because * - At least one indexed chain has a `chainStatus` of * {@link ChainIndexingStatusIds.Backfill}; and * - No indexed chain has a `chainStatus` of {@link ChainIndexingStatusIds.Following}. */ Backfill: "omnichain-backfill", /** * Represents that omnichain indexing is in an overall "following" status because * at least one indexed chain has a `chainStatus` of * {@link ChainIndexingStatusIds.Following}. */ Following: "omnichain-following", /** * Represents that omnichain indexing has completed because all indexed chains have * a `chainStatus` of {@link ChainIndexingStatusIds.Completed}. */ Completed: "omnichain-completed" }; var CrossChainIndexingStrategyIds = { /** * Represents that the indexing of events across all indexed chains will * proceed in a deterministic "omnichain" ordering by block timestamp, chain ID, * and block number. * * This strategy is "deterministic" in that the order of processing cross-chain indexed * events and each resulting indexed data state transition recorded in ENSDb is always * the same for each ENSIndexer instance operating with an equivalent * `ENSIndexerConfig` and ENSIndexer version. However it also has the drawbacks of: * - increased indexing latency that must wait for the slowest indexed chain to * add new blocks or to discover new blocks through the configured RPCs. * - if any indexed chain gets "stuck" due to chain or RPC failures, all indexed chains * will be affected. */ Omnichain: "omnichain" }; // src/shared/block-ref.ts function isBefore(blockA, blockB) { return blockA.number < blockB.number && blockA.timestamp < blockB.timestamp; } function isEqualTo(blockA, blockB) { return blockA.number === blockB.number && blockA.timestamp === blockB.timestamp; } function isBeforeOrEqualTo(blockA, blockB) { return isBefore(blockA, blockB) || isEqualTo(blockA, blockB); } // src/ensindexer/indexing-status/helpers.ts function getOmnichainIndexingStatus(chains) { if (checkChainIndexingStatusSnapshotsForOmnichainStatusSnapshotFollowing(chains)) { return OmnichainIndexingStatusIds.Following; } if (checkChainIndexingStatusSnapshotsForOmnichainStatusSnapshotBackfill(chains)) { return OmnichainIndexingStatusIds.Backfill; } if (checkChainIndexingStatusSnapshotsForOmnichainStatusSnapshotUnstarted(chains)) { return OmnichainIndexingStatusIds.Unstarted; } if (checkChainIndexingStatusSnapshotsForOmnichainStatusSnapshotCompleted(chains)) { return OmnichainIndexingStatusIds.Completed; } throw new Error(`Unable to determine omnichain indexing status for provided chains.`); } function getTimestampForLowestOmnichainStartBlock(chains) { const earliestKnownBlockTimestamps = chains.map( (chain) => chain.config.startBlock.timestamp ); return Math.min(...earliestKnownBlockTimestamps); } function getTimestampForHighestOmnichainKnownBlock(chains) { const latestKnownBlockTimestamps = []; for (const chain of chains) { switch (chain.chainStatus) { case ChainIndexingStatusIds.Queued: if (chain.config.configType === ChainIndexingConfigTypeIds.Definite && chain.config.endBlock) { latestKnownBlockTimestamps.push(chain.config.endBlock.timestamp); } break; case ChainIndexingStatusIds.Backfill: latestKnownBlockTimestamps.push(chain.backfillEndBlock.timestamp); break; case ChainIndexingStatusIds.Completed: latestKnownBlockTimestamps.push(chain.latestIndexedBlock.timestamp); break; case ChainIndexingStatusIds.Following: latestKnownBlockTimestamps.push(chain.latestKnownBlock.timestamp); break; } } return Math.max(...latestKnownBlockTimestamps); } function getOmnichainIndexingCursor(chains) { if (chains.length === 0) { throw new Error(`Unable to determine omnichain indexing cursor when no chains were provided.`); } if (getOmnichainIndexingStatus(chains) === OmnichainIndexingStatusIds.Unstarted) { const earliestStartBlockTimestamps = chains.map((chain) => chain.config.startBlock.timestamp); return Math.min(...earliestStartBlockTimestamps) - 1; } const latestIndexedBlockTimestamps = chains.filter((chain) => chain.chainStatus !== ChainIndexingStatusIds.Queued).map((chain) => chain.latestIndexedBlock.timestamp); if (latestIndexedBlockTimestamps.length < 1) { throw new Error("latestIndexedBlockTimestamps array must include at least one element"); } return Math.max(...latestIndexedBlockTimestamps); } function createIndexingConfig(startBlock, endBlock) { if (endBlock) { return { configType: ChainIndexingConfigTypeIds.Definite, startBlock, endBlock }; } return { configType: ChainIndexingConfigTypeIds.Indefinite, startBlock }; } function checkChainIndexingStatusSnapshotsForOmnichainStatusSnapshotUnstarted(chains) { return chains.every((chain) => chain.chainStatus === ChainIndexingStatusIds.Queued); } function checkChainIndexingStatusSnapshotsForOmnichainStatusSnapshotBackfill(chains) { const atLeastOneChainInTargetStatus = chains.some( (chain) => chain.chainStatus === ChainIndexingStatusIds.Backfill ); const otherChainsHaveValidStatuses = chains.every( (chain) => chain.chainStatus === ChainIndexingStatusIds.Queued || chain.chainStatus === ChainIndexingStatusIds.Backfill || chain.chainStatus === ChainIndexingStatusIds.Completed ); return atLeastOneChainInTargetStatus && otherChainsHaveValidStatuses; } function checkChainIndexingStatusSnapshotsForOmnichainStatusSnapshotCompleted(chains) { const allChainsHaveValidStatuses = chains.every( (chain) => chain.chainStatus === ChainIndexingStatusIds.Completed ); return allChainsHaveValidStatuses; } function checkChainIndexingStatusSnapshotsForOmnichainStatusSnapshotFollowing(chains) { const allChainsHaveValidStatuses = chains.some( (chain) => chain.chainStatus === ChainIndexingStatusIds.Following ); return allChainsHaveValidStatuses; } function sortChainStatusesByStartBlockAsc(chains) { chains.sort( ([, chainA], [, chainB]) => chainA.config.startBlock.timestamp - chainB.config.startBlock.timestamp ); return chains; } function getLatestIndexedBlockRef(indexingStatus, chainId) { const chainIndexingStatus = indexingStatus.omnichainSnapshot.chains.get(chainId); if (chainIndexingStatus === void 0) { return null; } if (chainIndexingStatus.chainStatus === ChainIndexingStatusIds.Queued) { return null; } return chainIndexingStatus.latestIndexedBlock; } // src/ensindexer/indexing-status/validations.ts function invariant_chainSnapshotQueuedBlocks(ctx) { const { config } = ctx.value; if (config.configType === ChainIndexingConfigTypeIds.Indefinite) { return; } if (config.endBlock && isBeforeOrEqualTo(config.startBlock, config.endBlock) === false) { ctx.issues.push({ code: "custom", input: ctx.value, message: "`config.startBlock` must be before or same as `config.endBlock`." }); } } function invariant_chainSnapshotBackfillBlocks(ctx) { const { config, latestIndexedBlock, backfillEndBlock } = ctx.value; if (isBeforeOrEqualTo(config.startBlock, latestIndexedBlock) === false) { ctx.issues.push({ code: "custom", input: ctx.value, message: "`config.startBlock` must be before or same as `latestIndexedBlock`." }); } if (isBeforeOrEqualTo(latestIndexedBlock, backfillEndBlock) === false) { ctx.issues.push({ code: "custom", input: ctx.value, message: "`latestIndexedBlock` must be before or same as `backfillEndBlock`." }); } if (config.configType === ChainIndexingConfigTypeIds.Indefinite) { return; } if (config.endBlock && isEqualTo(backfillEndBlock, config.endBlock) === false) { ctx.issues.push({ code: "custom", input: ctx.value, message: "`backfillEndBlock` must be the same as `config.endBlock`." }); } } function invariant_chainSnapshotCompletedBlocks(ctx) { const { config, latestIndexedBlock } = ctx.value; if (isBeforeOrEqualTo(config.startBlock, latestIndexedBlock) === false) { ctx.issues.push({ code: "custom", input: ctx.value, message: "`config.startBlock` must be before or same as `latestIndexedBlock`." }); } if (isBeforeOrEqualTo(latestIndexedBlock, config.endBlock) === false) { ctx.issues.push({ code: "custom", input: ctx.value, message: "`latestIndexedBlock` must be before or same as `config.endBlock`." }); } } function invariant_chainSnapshotFollowingBlocks(ctx) { const { config, latestIndexedBlock, latestKnownBlock } = ctx.value; if (isBeforeOrEqualTo(config.startBlock, latestIndexedBlock) === false) { ctx.issues.push({ code: "custom", input: ctx.value, message: "`config.startBlock` must be before or same as `latestIndexedBlock`." }); } if (isBeforeOrEqualTo(latestIndexedBlock, latestKnownBlock) === false) { ctx.issues.push({ code: "custom", input: ctx.value, message: "`latestIndexedBlock` must be before or same as `latestKnownBlock`." }); } } function invariant_omnichainSnapshotStatusIsConsistentWithChainSnapshot(ctx) { const snapshot = ctx.value; const chains = Array.from(snapshot.chains.values()); const expectedOmnichainStatus = getOmnichainIndexingStatus(chains); const actualOmnichainStatus = snapshot.omnichainStatus; if (expectedOmnichainStatus !== actualOmnichainStatus) { ctx.issues.push({ code: "custom", input: snapshot, message: `'${actualOmnichainStatus}' is an invalid omnichainStatus. Expected '${expectedOmnichainStatus}' based on the statuses of individual chains.` }); } } function invariant_omnichainIndexingCursorLowerThanEarliestStartBlockAcrossQueuedChains(ctx) { const snapshot = ctx.value; const queuedChains = Array.from(snapshot.chains.values()).filter( (chain) => chain.chainStatus === ChainIndexingStatusIds.Queued ); if (queuedChains.length === 0) { return; } const queuedChainStartBlocks = queuedChains.map((chain) => chain.config.startBlock.timestamp); const queuedChainEarliestStartBlock = Math.min(...queuedChainStartBlocks); if (snapshot.omnichainIndexingCursor >= queuedChainEarliestStartBlock) { ctx.issues.push({ code: "custom", input: snapshot, message: "`omnichainIndexingCursor` must be lower than the earliest start block across all queued chains." }); } } function invariant_omnichainIndexingCursorLowerThanOrEqualToLatestBackfillEndBlockAcrossBackfillChains(ctx) { const snapshot = ctx.value; const backfillChains = Array.from(snapshot.chains.values()).filter( (chain) => chain.chainStatus === ChainIndexingStatusIds.Backfill ); if (backfillChains.length === 0) { return; } const backfillEndBlocks = backfillChains.map((chain) => chain.backfillEndBlock.timestamp); const highestBackfillEndBlock = Math.max(...backfillEndBlocks); if (snapshot.omnichainIndexingCursor > highestBackfillEndBlock) { ctx.issues.push({ code: "custom", input: snapshot, message: "`omnichainIndexingCursor` must be lower than or equal to the highest `backfillEndBlock` across all backfill chains." }); } } function invariant_omnichainIndexingCursorIsEqualToHighestLatestIndexedBlockAcrossIndexedChain(ctx) { const snapshot = ctx.value; const indexedChains = Array.from(snapshot.chains.values()).filter( (chain) => chain.chainStatus === ChainIndexingStatusIds.Backfill || chain.chainStatus === ChainIndexingStatusIds.Completed || chain.chainStatus === ChainIndexingStatusIds.Following ); if (indexedChains.length === 0) { return; } const indexedChainLatestIndexedBlocks = indexedChains.map( (chain) => chain.latestIndexedBlock.timestamp ); const indexedChainHighestLatestIndexedBlock = Math.max(...indexedChainLatestIndexedBlocks); if (snapshot.omnichainIndexingCursor !== indexedChainHighestLatestIndexedBlock) { ctx.issues.push({ code: "custom", input: snapshot, message: "`omnichainIndexingCursor` must be same as the highest `latestIndexedBlock` across all indexed chains." }); } } function invariant_omnichainSnapshotUnstartedHasValidChains(ctx) { const chains = ctx.value; const hasValidChains = checkChainIndexingStatusSnapshotsForOmnicha