From 58e6ab00b02f65d3b7ea065e4b5aecd1db55b739 Mon Sep 17 00:00:00 2001 From: Zimtente Date: Mon, 14 Jul 2025 08:32:57 +0200 Subject: [PATCH 01/61] added seaport orderfulfill event --- .gitattributes | 2 + apps/ensindexer/package.json | 3 +- apps/ensindexer/src/handlers/Seaport.ts | 165 ++ .../src/lib/seaport/seaport-helpers.ts | 36 + apps/ensindexer/src/lib/seaport/v1-6/types.ts | 128 ++ apps/ensindexer/src/plugins/index.ts | 2 + .../src/plugins/seaport/event-handlers.ts | 9 + .../src/plugins/seaport/handlers/Seaport.ts | 15 + apps/ensindexer/src/plugins/seaport/plugin.ts | 44 + .../datasources/src/abis/seaport/Seaport.ts | 2002 +++++++++++++++++ packages/datasources/src/lib/types.ts | 1 + packages/datasources/src/mainnet.ts | 14 + packages/datasources/src/sepolia.ts | 14 +- packages/ensnode-schema/src/ponder.schema.ts | 1 + packages/ensnode-schema/src/seaport.schema.ts | 35 + packages/ensnode-sdk/src/utils/types.ts | 1 + pnpm-lock.yaml | 89 + 17 files changed, 2559 insertions(+), 2 deletions(-) create mode 100644 .gitattributes create mode 100644 apps/ensindexer/src/handlers/Seaport.ts create mode 100644 apps/ensindexer/src/lib/seaport/seaport-helpers.ts create mode 100644 apps/ensindexer/src/lib/seaport/v1-6/types.ts create mode 100644 apps/ensindexer/src/plugins/seaport/event-handlers.ts create mode 100644 apps/ensindexer/src/plugins/seaport/handlers/Seaport.ts create mode 100644 apps/ensindexer/src/plugins/seaport/plugin.ts create mode 100644 packages/datasources/src/abis/seaport/Seaport.ts create mode 100644 packages/ensnode-schema/src/seaport.schema.ts diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 000000000..eba1110b5 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,2 @@ +# Auto detect text files and perform LF normalization +* text=auto \ No newline at end of file diff --git a/apps/ensindexer/package.json b/apps/ensindexer/package.json index 6c81de716..b2978c233 100644 --- a/apps/ensindexer/package.json +++ b/apps/ensindexer/package.json @@ -26,10 +26,11 @@ "@ensdomains/ensjs": "^4.0.2", "@ensnode/datasources": "workspace:*", "@ensnode/ensnode-schema": "workspace:*", + "@ensnode/ensnode-sdk": "workspace:*", "@ensnode/ensrainbow-sdk": "workspace:*", "@ensnode/ponder-metadata": "workspace:*", "@ensnode/ponder-subgraph": "workspace:*", - "@ensnode/ensnode-sdk": "workspace:*", + "@opensea/seaport-js": "^4.0.5", "@types/dns-packet": "^5.6.5", "deepmerge-ts": "^7.1.5", "dns-packet": "^5.6.1", diff --git a/apps/ensindexer/src/handlers/Seaport.ts b/apps/ensindexer/src/handlers/Seaport.ts new file mode 100644 index 000000000..81a9b0d38 --- /dev/null +++ b/apps/ensindexer/src/handlers/Seaport.ts @@ -0,0 +1,165 @@ +import {Context} from "ponder:registry"; +import schema from "ponder:schema"; +import {ConsiderationItem, OfferItem} from "@opensea/seaport-js/lib/types"; +import {ItemType} from "@opensea/seaport-js/lib/constants"; + +import {sharedEventValues, upsertAccount} from "@/lib/db-helpers"; +import {EventWithArgs} from "@/lib/ponder-helpers"; +import {upsertCurrency} from "@/lib/seaport/seaport-helpers"; + +// Supported contracts +const SUPPORTED_CONTRACTS = [ + "0x57f1887a8bf19b14fc0df6fd9b2acc9af147ea85", + "0x0635513f179D50A207757E05759CbD106d7dFcE8", +]; + +interface SeaportOrderFulfilledEvent extends EventWithArgs<{ + orderHash: string; + offerer: string; + zone: string; + recipient: string; + offer: OfferItem[]; + consideration: ConsiderationItem[]; +}> { +} + +/** + * Handles NFT offers being fulfilled (someone accepting an offer) + * In an offer: NFT holder accepts someone's offer to buy their NFT + * - NFT is in consideration (what the offerer wants) + * - Payment is in offer (what the offerer gives) + */ +async function handleOffer( + context: Context, + event: SeaportOrderFulfilledEvent, + nftItem: ConsiderationItem, + payment: OfferItem +) { + const {orderHash, offerer, recipient} = event.args; + + // In an offer, the offerer is buying the NFT, recipient is selling + const buyer = offerer; + const seller = recipient; + + // Ensure accounts exist + await upsertAccount(context, buyer); + await upsertAccount(context, seller); + + // Get currency info + const currencyId = await upsertCurrency(context, payment.token); + + // Record the sale + await context.db.insert(schema.nameSold).values({ + ...sharedEventValues(context.chain.id, event), + fromOwnerId: seller, + newOwnerId: buyer, + currencyId: currencyId, + chainId: context.chain.id, + orderHash: orderHash, + price: BigInt(payment.amount), + tokenContract: nftItem.token, + tokenId: nftItem.identifier.toString(), + itemType: nftItem.itemType === ItemType.ERC721 ? "ERC721" : "ERC1155", + }); +} + +/** + * Handles NFT listings being fulfilled (someone buying a listed item) + * In a listing: NFT owner lists their NFT for sale + * - NFT is in offer (what the offerer gives) + * - Payment is in consideration (what the offerer wants) + */ +async function handleListing( + context: Context, + event: SeaportOrderFulfilledEvent, + nftItem: OfferItem, + payment: ConsiderationItem +) { + const {orderHash, offerer, recipient} = event.args; + + // In a listing, the offerer is selling the NFT, recipient is buying + const seller = offerer; + const buyer = recipient; + + // Ensure accounts exist + await upsertAccount(context, seller); + await upsertAccount(context, buyer); + + // Get currency info + const currencyId = await upsertCurrency(context, payment.token); + + // Record the sale + await context.db.insert(schema.nameSold).values({ + ...sharedEventValues(context.chain.id, event), + fromOwnerId: seller, + newOwnerId: buyer, + currencyId: currencyId, + chainId: context.chain.id, + orderHash: orderHash, + price: BigInt(payment.amount), + tokenContract: nftItem.token, + tokenId: nftItem.identifier.toString(), + itemType: nftItem.itemType === ItemType.ERC721 ? "ERC721" : "ERC1155", + }); +} + +/** + * Validates if an NFT item is supported + */ +function isValidNFTItem(item: OfferItem | ConsiderationItem): boolean { + if (!item || !item.token) return false; + + const isValidItemType = item.itemType === ItemType.ERC721 || item.itemType === ItemType.ERC1155; + const isSupportedContract = SUPPORTED_CONTRACTS.includes(item.token); + + return isValidItemType && isSupportedContract; +} + +/** + * Finds the payment item from offer or consideration arrays + */ +function findPaymentInOffer(offer: OfferItem[]): OfferItem | undefined { + return offer.find( + (item) => item.itemType === ItemType.NATIVE || item.itemType === ItemType.ERC20 + ); +} + +/** + * Finds the payment item from consideration array (only support NATIVE and ERC20) + */ +function findPaymentInConsideration(consideration: ConsiderationItem[]): ConsiderationItem | undefined { + return consideration.find( + (item) => item.itemType === ItemType.NATIVE || item.itemType === ItemType.ERC20 + ); +} + +/** + * Main handler for Seaport OrderFulfilled events + */ +export async function handleOrderFulfilled({ + context, + event, + }: { + context: Context; + event: SeaportOrderFulfilledEvent; +}) { + const {offer, consideration} = event.args; + + // Check if this is a listing (NFT in offer, payment in consideration) + const nftInOffer = offer.find(isValidNFTItem); + const paymentInConsideration = findPaymentInConsideration(consideration); + + if (nftInOffer && paymentInConsideration) { + await handleListing(context, event, nftInOffer, paymentInConsideration); + return; + } + + // Check if this is an offer (payment in offer, NFT in consideration) + const paymentInOffer = findPaymentInOffer(offer); + const nftInConsideration = consideration.find(isValidNFTItem); + + if (paymentInOffer && nftInConsideration) { + await handleOffer(context, event, nftInConsideration, paymentInOffer); + return; + } +} \ No newline at end of file diff --git a/apps/ensindexer/src/lib/seaport/seaport-helpers.ts b/apps/ensindexer/src/lib/seaport/seaport-helpers.ts new file mode 100644 index 000000000..673ee7c86 --- /dev/null +++ b/apps/ensindexer/src/lib/seaport/seaport-helpers.ts @@ -0,0 +1,36 @@ +import { Context } from "ponder:registry"; +import { Address, Hex, zeroAddress } from "viem"; +import schema from "ponder:schema"; + +export async function upsertCurrency(context: Context, tokenAddress: Address): Promise { + const currencyId = tokenAddress as Hex; + + const existingCurrency = await context.db.find(schema.currency, { + id: currencyId, + }); + + if (!existingCurrency) { + if (tokenAddress === zeroAddress) { + await context.db.insert(schema.currency).values({ + id: currencyId, + name: "Ether", + symbol: "ETH", + decimals: 18, + contractAddress: zeroAddress, + chainId: context.chain.id, + }); + } else { + // may have to fetch tokenMetadata from a provider (coingecko, binance, etc) + await context.db.insert(schema.currency).values({ + id: currencyId, + name: null, + symbol: null, + decimals: 18, + contractAddress: tokenAddress, + chainId: context.chain.id, + }); + } + } + + return currencyId; +} \ No newline at end of file diff --git a/apps/ensindexer/src/lib/seaport/v1-6/types.ts b/apps/ensindexer/src/lib/seaport/v1-6/types.ts new file mode 100644 index 000000000..87e55bf4c --- /dev/null +++ b/apps/ensindexer/src/lib/seaport/v1-6/types.ts @@ -0,0 +1,128 @@ +export type OfferItem = { + itemType: ItemType; + token: string; + identifierOrCriteria: string; + startAmount: string; + endAmount: string; +}; + +export type ConsiderationItem = { + itemType: ItemType; + token: string; + identifierOrCriteria: string; + startAmount: string; + endAmount: string; + recipient: string; +}; + +export type Item = OfferItem | ConsiderationItem; + +export type OrderParameters = { + offerer: string; + zone: string; + orderType: OrderType; + startTime: BigNumberish; + endTime: BigNumberish; + zoneHash: string; + salt: string; + offer: OfferItem[]; + consideration: ConsiderationItem[]; + totalOriginalConsiderationItems: BigNumberish; + conduitKey: string; +}; + +export type OrderComponents = OrderParameters & { counter: BigNumberish }; + +export type Order = { + parameters: OrderParameters; + signature: string; +}; + +export type AdvancedOrder = Order & { + numerator: bigint; + denominator: bigint; + extraData: string; +}; + +export type BasicErc721Item = { + itemType: ItemType.ERC721; + token: string; + identifier: string; +}; + +export type Erc721ItemWithCriteria = { + itemType: ItemType.ERC721; + token: string; + amount?: string; + endAmount?: string; + // Used for criteria based items i.e. offering to buy 5 NFTs for a collection +} & ({ identifiers: string[] } | { criteria: string }); + +type Erc721Item = BasicErc721Item | Erc721ItemWithCriteria; + +export type BasicErc1155Item = { + itemType: ItemType.ERC1155; + token: string; + identifier: string; + amount: string; + endAmount?: string; +}; + +export type Erc1155ItemWithCriteria = { + itemType: ItemType.ERC1155; + token: string; + amount: string; + endAmount?: string; +} & ({ identifiers: string[] } | { criteria: string }); + +type Erc1155Item = BasicErc1155Item | Erc1155ItemWithCriteria; + +export type CurrencyItem = { + token?: string; + amount: string; + endAmount?: string; +}; + +export type CreateInputItem = Erc721Item | Erc1155Item | CurrencyItem; + +export type ConsiderationInputItem = CreateInputItem & { recipient?: string }; + +export type TipInputItem = CreateInputItem & { recipient: string }; + +export type Fee = { + recipient: string; + basisPoints: number; +}; + +export type CreateOrderInput = { + conduitKey?: string; + zone?: string; + zoneHash?: string; + startTime?: BigNumberish; + endTime?: BigNumberish; + offer: readonly CreateInputItem[]; + consideration: readonly ConsiderationInputItem[]; + counter?: BigNumberish; + fees?: readonly Fee[]; + allowPartialFills?: boolean; + restrictedByZone?: boolean; + domain?: string; + salt?: BigNumberish; +}; + +export type InputCriteria = { + identifier: string; + proof: string[]; +}; + +export type OrderStatus = { + isValidated: boolean; + isCancelled: boolean; + totalFilled: bigint; + totalSize: bigint; +}; + +export type OrderWithCounter = { + parameters: OrderComponents; + signature: string; +}; diff --git a/apps/ensindexer/src/plugins/index.ts b/apps/ensindexer/src/plugins/index.ts index 3b4e218fd..167d1c605 100644 --- a/apps/ensindexer/src/plugins/index.ts +++ b/apps/ensindexer/src/plugins/index.ts @@ -3,6 +3,7 @@ import { PluginName } from "@ensnode/ensnode-sdk"; import type { MergedTypes } from "@/lib/lib-helpers"; import basenamesPlugin from "./basenames/plugin"; import lineaNamesPlugin from "./lineanames/plugin"; +import seaportPlugin from "./seaport/plugin"; import subgraphPlugin from "./subgraph/plugin"; import threednsPlugin from "./threedns/plugin"; @@ -11,6 +12,7 @@ export const ALL_PLUGINS = [ basenamesPlugin, lineaNamesPlugin, threednsPlugin, + seaportPlugin, ] as const; /** diff --git a/apps/ensindexer/src/plugins/seaport/event-handlers.ts b/apps/ensindexer/src/plugins/seaport/event-handlers.ts new file mode 100644 index 000000000..512898cf6 --- /dev/null +++ b/apps/ensindexer/src/plugins/seaport/event-handlers.ts @@ -0,0 +1,9 @@ +import config from "@/config"; +import { PluginName } from "@ensnode/ensnode-sdk"; + +import attach_Seaport from "./handlers/Seaport"; + +// conditionally attach event handlers when Ponder executes this file +if (config.plugins.includes(PluginName.Seaport)) { + attach_Seaport(); +} diff --git a/apps/ensindexer/src/plugins/seaport/handlers/Seaport.ts b/apps/ensindexer/src/plugins/seaport/handlers/Seaport.ts new file mode 100644 index 000000000..2505fdc73 --- /dev/null +++ b/apps/ensindexer/src/plugins/seaport/handlers/Seaport.ts @@ -0,0 +1,15 @@ +import { ponder } from "ponder:registry"; + +import { PluginName } from "@ensnode/ensnode-sdk"; + +import {handleOrderFulfilled} from "@/handlers/Seaport"; +import { namespaceContract } from "@/lib/plugin-helpers"; + +/** + * Registers event handlers with Ponder. + */ +export default function () { + const pluginName = PluginName.Seaport; + + ponder.on(namespaceContract(pluginName, "Seaport:OrderFulfilled"), handleOrderFulfilled); +} diff --git a/apps/ensindexer/src/plugins/seaport/plugin.ts b/apps/ensindexer/src/plugins/seaport/plugin.ts new file mode 100644 index 000000000..3f0e3a1a5 --- /dev/null +++ b/apps/ensindexer/src/plugins/seaport/plugin.ts @@ -0,0 +1,44 @@ +/** + * The Seaport plugin describes indexing behavior for Seaport contracts on all supported networks. + */ + +import { + createPlugin, + getDatasourceAsFullyDefinedAtCompileTime, + namespaceContract, +} from "@/lib/plugin-helpers"; +import { chainConfigForContract, chainConnectionConfig } from "@/lib/ponder-helpers"; +import { DatasourceNames } from "@ensnode/datasources"; +import { PluginName } from "@ensnode/ensnode-sdk"; +import * as ponder from "ponder"; + +const pluginName = PluginName.Seaport; + +export default createPlugin({ + name: pluginName, + requiredDatasourceNames: [DatasourceNames.Seaport], + createPonderConfig(config) { + const seaport = getDatasourceAsFullyDefinedAtCompileTime( + config.namespace, + DatasourceNames.Seaport, + ); + + return ponder.createConfig({ + chains: { + ...chainConnectionConfig(config.rpcConfigs, seaport.chain.id), + }, + contracts: { + [namespaceContract(pluginName, "Seaport")]: { + chain: { + ...chainConfigForContract( + config.globalBlockrange, + seaport.chain.id, + seaport.contracts.Seaport, + ), + }, + abi: seaport.contracts.Seaport.abi, + }, + }, + }); + }, +}); diff --git a/packages/datasources/src/abis/seaport/Seaport.ts b/packages/datasources/src/abis/seaport/Seaport.ts new file mode 100644 index 000000000..ddb0313be --- /dev/null +++ b/packages/datasources/src/abis/seaport/Seaport.ts @@ -0,0 +1,2002 @@ +export const Seaport = [ + { + inputs: [{ internalType: 'address', name: 'conduitController', type: 'address' }], + stateMutability: 'nonpayable', + type: 'constructor', + }, + { inputs: [], name: 'BadContractSignature', type: 'error' }, + { + inputs: [], + name: 'BadFraction', + type: 'error', + }, + { + inputs: [ + { internalType: 'address', name: 'token', type: 'address' }, + { + internalType: 'address', + name: 'from', + type: 'address', + }, + { internalType: 'address', name: 'to', type: 'address' }, + { + internalType: 'uint256', + name: 'amount', + type: 'uint256', + }, + ], + name: 'BadReturnValueFromERC20OnTransfer', + type: 'error', + }, + { + inputs: [{ internalType: 'uint8', name: 'v', type: 'uint8' }], + name: 'BadSignatureV', + type: 'error', + }, + { inputs: [], name: 'CannotCancelOrder', type: 'error' }, + { + inputs: [], + name: 'ConsiderationCriteriaResolverOutOfRange', + type: 'error', + }, + { + inputs: [], + name: 'ConsiderationLengthNotEqualToTotalOriginal', + type: 'error', + }, + { + inputs: [ + { internalType: 'uint256', name: 'orderIndex', type: 'uint256' }, + { + internalType: 'uint256', + name: 'considerationIndex', + type: 'uint256', + }, + { internalType: 'uint256', name: 'shortfallAmount', type: 'uint256' }, + ], + name: 'ConsiderationNotMet', + type: 'error', + }, + { inputs: [], name: 'CriteriaNotEnabledForItem', type: 'error' }, + { + inputs: [ + { + internalType: 'address', + name: 'token', + type: 'address', + }, + { internalType: 'address', name: 'from', type: 'address' }, + { + internalType: 'address', + name: 'to', + type: 'address', + }, + { internalType: 'uint256[]', name: 'identifiers', type: 'uint256[]' }, + { + internalType: 'uint256[]', + name: 'amounts', + type: 'uint256[]', + }, + ], + name: 'ERC1155BatchTransferGenericFailure', + type: 'error', + }, + { inputs: [], name: 'InexactFraction', type: 'error' }, + { + inputs: [], + name: 'InsufficientNativeTokensSupplied', + type: 'error', + }, + { inputs: [], name: 'Invalid1155BatchTransferEncoding', type: 'error' }, + { + inputs: [], + name: 'InvalidBasicOrderParameterEncoding', + type: 'error', + }, + { + inputs: [{ internalType: 'address', name: 'conduit', type: 'address' }], + name: 'InvalidCallToConduit', + type: 'error', + }, + { + inputs: [ + { internalType: 'bytes32', name: 'conduitKey', type: 'bytes32' }, + { + internalType: 'address', + name: 'conduit', + type: 'address', + }, + ], + name: 'InvalidConduit', + type: 'error', + }, + { + inputs: [{ internalType: 'bytes32', name: 'orderHash', type: 'bytes32' }], + name: 'InvalidContractOrder', + type: 'error', + }, + { + inputs: [{ internalType: 'uint256', name: 'amount', type: 'uint256' }], + name: 'InvalidERC721TransferAmount', + type: 'error', + }, + { inputs: [], name: 'InvalidFulfillmentComponentData', type: 'error' }, + { + inputs: [ + { + internalType: 'uint256', + name: 'value', + type: 'uint256', + }, + ], + name: 'InvalidMsgValue', + type: 'error', + }, + { inputs: [], name: 'InvalidNativeOfferItem', type: 'error' }, + { + inputs: [], + name: 'InvalidProof', + type: 'error', + }, + { + inputs: [{ internalType: 'bytes32', name: 'orderHash', type: 'bytes32' }], + name: 'InvalidRestrictedOrder', + type: 'error', + }, + { inputs: [], name: 'InvalidSignature', type: 'error' }, + { + inputs: [], + name: 'InvalidSigner', + type: 'error', + }, + { + inputs: [ + { internalType: 'uint256', name: 'startTime', type: 'uint256' }, + { + internalType: 'uint256', + name: 'endTime', + type: 'uint256', + }, + ], + name: 'InvalidTime', + type: 'error', + }, + { + inputs: [{ internalType: 'uint256', name: 'fulfillmentIndex', type: 'uint256' }], + name: 'MismatchedFulfillmentOfferAndConsiderationComponents', + type: 'error', + }, + { + inputs: [{ internalType: 'enum Side', name: 'side', type: 'uint8' }], + name: 'MissingFulfillmentComponentOnAggregation', + type: 'error', + }, + { inputs: [], name: 'MissingItemAmount', type: 'error' }, + { + inputs: [], + name: 'MissingOriginalConsiderationItems', + type: 'error', + }, + { + inputs: [ + { internalType: 'address', name: 'account', type: 'address' }, + { + internalType: 'uint256', + name: 'amount', + type: 'uint256', + }, + ], + name: 'NativeTokenTransferGenericFailure', + type: 'error', + }, + { + inputs: [{ internalType: 'address', name: 'account', type: 'address' }], + name: 'NoContract', + type: 'error', + }, + { inputs: [], name: 'NoReentrantCalls', type: 'error' }, + { + inputs: [], + name: 'NoSpecifiedOrdersAvailable', + type: 'error', + }, + { inputs: [], name: 'OfferAndConsiderationRequiredOnFulfillment', type: 'error' }, + { + inputs: [], + name: 'OfferCriteriaResolverOutOfRange', + type: 'error', + }, + { + inputs: [{ internalType: 'bytes32', name: 'orderHash', type: 'bytes32' }], + name: 'OrderAlreadyFilled', + type: 'error', + }, + { + inputs: [{ internalType: 'enum Side', name: 'side', type: 'uint8' }], + name: 'OrderCriteriaResolverOutOfRange', + type: 'error', + }, + { + inputs: [{ internalType: 'bytes32', name: 'orderHash', type: 'bytes32' }], + name: 'OrderIsCancelled', + type: 'error', + }, + { + inputs: [{ internalType: 'bytes32', name: 'orderHash', type: 'bytes32' }], + name: 'OrderPartiallyFilled', + type: 'error', + }, + { inputs: [], name: 'PartialFillsNotEnabledForOrder', type: 'error' }, + { + inputs: [ + { + internalType: 'address', + name: 'token', + type: 'address', + }, + { internalType: 'address', name: 'from', type: 'address' }, + { + internalType: 'address', + name: 'to', + type: 'address', + }, + { internalType: 'uint256', name: 'identifier', type: 'uint256' }, + { + internalType: 'uint256', + name: 'amount', + type: 'uint256', + }, + ], + name: 'TokenTransferGenericFailure', + type: 'error', + }, + { + inputs: [ + { internalType: 'uint256', name: 'orderIndex', type: 'uint256' }, + { + internalType: 'uint256', + name: 'considerationIndex', + type: 'uint256', + }, + ], + name: 'UnresolvedConsiderationCriteria', + type: 'error', + }, + { + inputs: [ + { internalType: 'uint256', name: 'orderIndex', type: 'uint256' }, + { + internalType: 'uint256', + name: 'offerIndex', + type: 'uint256', + }, + ], + name: 'UnresolvedOfferCriteria', + type: 'error', + }, + { inputs: [], name: 'UnusedItemParameters', type: 'error' }, + { + anonymous: false, + inputs: [ + { indexed: false, internalType: 'uint256', name: 'newCounter', type: 'uint256' }, + { + indexed: true, + internalType: 'address', + name: 'offerer', + type: 'address', + }, + ], + name: 'CounterIncremented', + type: 'event', + }, + { + anonymous: false, + inputs: [ + { indexed: false, internalType: 'bytes32', name: 'orderHash', type: 'bytes32' }, + { + indexed: true, + internalType: 'address', + name: 'offerer', + type: 'address', + }, + { indexed: true, internalType: 'address', name: 'zone', type: 'address' }, + ], + name: 'OrderCancelled', + type: 'event', + }, + { + anonymous: false, + inputs: [ + { indexed: false, internalType: 'bytes32', name: 'orderHash', type: 'bytes32' }, + { + indexed: true, + internalType: 'address', + name: 'offerer', + type: 'address', + }, + { indexed: true, internalType: 'address', name: 'zone', type: 'address' }, + { + indexed: false, + internalType: 'address', + name: 'recipient', + type: 'address', + }, + { + components: [ + { + internalType: 'enum ItemType', + name: 'itemType', + type: 'uint8', + }, + { internalType: 'address', name: 'token', type: 'address' }, + { + internalType: 'uint256', + name: 'identifier', + type: 'uint256', + }, + { internalType: 'uint256', name: 'amount', type: 'uint256' }, + ], + indexed: false, + internalType: 'struct SpentItem[]', + name: 'offer', + type: 'tuple[]', + }, + { + components: [ + { + internalType: 'enum ItemType', + name: 'itemType', + type: 'uint8', + }, + { internalType: 'address', name: 'token', type: 'address' }, + { + internalType: 'uint256', + name: 'identifier', + type: 'uint256', + }, + { internalType: 'uint256', name: 'amount', type: 'uint256' }, + { + internalType: 'address payable', + name: 'recipient', + type: 'address', + }, + ], + indexed: false, + internalType: 'struct ReceivedItem[]', + name: 'consideration', + type: 'tuple[]', + }, + ], + name: 'OrderFulfilled', + type: 'event', + }, + { + anonymous: false, + inputs: [ + { indexed: false, internalType: 'bytes32', name: 'orderHash', type: 'bytes32' }, + { + components: [ + { internalType: 'address', name: 'offerer', type: 'address' }, + { + internalType: 'address', + name: 'zone', + type: 'address', + }, + { + components: [ + { + internalType: 'enum ItemType', + name: 'itemType', + type: 'uint8', + }, + { internalType: 'address', name: 'token', type: 'address' }, + { + internalType: 'uint256', + name: 'identifierOrCriteria', + type: 'uint256', + }, + { internalType: 'uint256', name: 'startAmount', type: 'uint256' }, + { + internalType: 'uint256', + name: 'endAmount', + type: 'uint256', + }, + ], + internalType: 'struct OfferItem[]', + name: 'offer', + type: 'tuple[]', + }, + { + components: [ + { + internalType: 'enum ItemType', + name: 'itemType', + type: 'uint8', + }, + { internalType: 'address', name: 'token', type: 'address' }, + { + internalType: 'uint256', + name: 'identifierOrCriteria', + type: 'uint256', + }, + { internalType: 'uint256', name: 'startAmount', type: 'uint256' }, + { + internalType: 'uint256', + name: 'endAmount', + type: 'uint256', + }, + { internalType: 'address payable', name: 'recipient', type: 'address' }, + ], + internalType: 'struct ConsiderationItem[]', + name: 'consideration', + type: 'tuple[]', + }, + { internalType: 'enum OrderType', name: 'orderType', type: 'uint8' }, + { + internalType: 'uint256', + name: 'startTime', + type: 'uint256', + }, + { internalType: 'uint256', name: 'endTime', type: 'uint256' }, + { + internalType: 'bytes32', + name: 'zoneHash', + type: 'bytes32', + }, + { internalType: 'uint256', name: 'salt', type: 'uint256' }, + { + internalType: 'bytes32', + name: 'conduitKey', + type: 'bytes32', + }, + { internalType: 'uint256', name: 'totalOriginalConsiderationItems', type: 'uint256' }, + ], + indexed: false, + internalType: 'struct OrderParameters', + name: 'orderParameters', + type: 'tuple', + }, + ], + name: 'OrderValidated', + type: 'event', + }, + { + anonymous: false, + inputs: [{ indexed: false, internalType: 'bytes32[]', name: 'orderHashes', type: 'bytes32[]' }], + name: 'OrdersMatched', + type: 'event', + }, + { + inputs: [ + { + components: [ + { internalType: 'address', name: 'offerer', type: 'address' }, + { + internalType: 'address', + name: 'zone', + type: 'address', + }, + { + components: [ + { + internalType: 'enum ItemType', + name: 'itemType', + type: 'uint8', + }, + { internalType: 'address', name: 'token', type: 'address' }, + { + internalType: 'uint256', + name: 'identifierOrCriteria', + type: 'uint256', + }, + { internalType: 'uint256', name: 'startAmount', type: 'uint256' }, + { + internalType: 'uint256', + name: 'endAmount', + type: 'uint256', + }, + ], + internalType: 'struct OfferItem[]', + name: 'offer', + type: 'tuple[]', + }, + { + components: [ + { + internalType: 'enum ItemType', + name: 'itemType', + type: 'uint8', + }, + { internalType: 'address', name: 'token', type: 'address' }, + { + internalType: 'uint256', + name: 'identifierOrCriteria', + type: 'uint256', + }, + { internalType: 'uint256', name: 'startAmount', type: 'uint256' }, + { + internalType: 'uint256', + name: 'endAmount', + type: 'uint256', + }, + { internalType: 'address payable', name: 'recipient', type: 'address' }, + ], + internalType: 'struct ConsiderationItem[]', + name: 'consideration', + type: 'tuple[]', + }, + { internalType: 'enum OrderType', name: 'orderType', type: 'uint8' }, + { + internalType: 'uint256', + name: 'startTime', + type: 'uint256', + }, + { internalType: 'uint256', name: 'endTime', type: 'uint256' }, + { + internalType: 'bytes32', + name: 'zoneHash', + type: 'bytes32', + }, + { internalType: 'uint256', name: 'salt', type: 'uint256' }, + { + internalType: 'bytes32', + name: 'conduitKey', + type: 'bytes32', + }, + { internalType: 'uint256', name: 'counter', type: 'uint256' }, + ], + internalType: 'struct OrderComponents[]', + name: 'orders', + type: 'tuple[]', + }, + ], + name: 'cancel', + outputs: [{ internalType: 'bool', name: 'cancelled', type: 'bool' }], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [ + { + components: [ + { + components: [ + { + internalType: 'address', + name: 'offerer', + type: 'address', + }, + { + internalType: 'address', + name: 'zone', + type: 'address', + }, + { + components: [ + { + internalType: 'enum ItemType', + name: 'itemType', + type: 'uint8', + }, + { internalType: 'address', name: 'token', type: 'address' }, + { + internalType: 'uint256', + name: 'identifierOrCriteria', + type: 'uint256', + }, + { internalType: 'uint256', name: 'startAmount', type: 'uint256' }, + { + internalType: 'uint256', + name: 'endAmount', + type: 'uint256', + }, + ], + internalType: 'struct OfferItem[]', + name: 'offer', + type: 'tuple[]', + }, + { + components: [ + { + internalType: 'enum ItemType', + name: 'itemType', + type: 'uint8', + }, + { internalType: 'address', name: 'token', type: 'address' }, + { + internalType: 'uint256', + name: 'identifierOrCriteria', + type: 'uint256', + }, + { internalType: 'uint256', name: 'startAmount', type: 'uint256' }, + { + internalType: 'uint256', + name: 'endAmount', + type: 'uint256', + }, + { internalType: 'address payable', name: 'recipient', type: 'address' }, + ], + internalType: 'struct ConsiderationItem[]', + name: 'consideration', + type: 'tuple[]', + }, + { internalType: 'enum OrderType', name: 'orderType', type: 'uint8' }, + { + internalType: 'uint256', + name: 'startTime', + type: 'uint256', + }, + { internalType: 'uint256', name: 'endTime', type: 'uint256' }, + { + internalType: 'bytes32', + name: 'zoneHash', + type: 'bytes32', + }, + { internalType: 'uint256', name: 'salt', type: 'uint256' }, + { + internalType: 'bytes32', + name: 'conduitKey', + type: 'bytes32', + }, + { internalType: 'uint256', name: 'totalOriginalConsiderationItems', type: 'uint256' }, + ], + internalType: 'struct OrderParameters', + name: 'parameters', + type: 'tuple', + }, + { internalType: 'uint120', name: 'numerator', type: 'uint120' }, + { + internalType: 'uint120', + name: 'denominator', + type: 'uint120', + }, + { internalType: 'bytes', name: 'signature', type: 'bytes' }, + { + internalType: 'bytes', + name: 'extraData', + type: 'bytes', + }, + ], + internalType: 'struct AdvancedOrder', + name: '', + type: 'tuple', + }, + { + components: [ + { + internalType: 'uint256', + name: 'orderIndex', + type: 'uint256', + }, + { internalType: 'enum Side', name: 'side', type: 'uint8' }, + { + internalType: 'uint256', + name: 'index', + type: 'uint256', + }, + { internalType: 'uint256', name: 'identifier', type: 'uint256' }, + { + internalType: 'bytes32[]', + name: 'criteriaProof', + type: 'bytes32[]', + }, + ], + internalType: 'struct CriteriaResolver[]', + name: '', + type: 'tuple[]', + }, + { internalType: 'bytes32', name: 'fulfillerConduitKey', type: 'bytes32' }, + { + internalType: 'address', + name: 'recipient', + type: 'address', + }, + ], + name: 'fulfillAdvancedOrder', + outputs: [{ internalType: 'bool', name: 'fulfilled', type: 'bool' }], + stateMutability: 'payable', + type: 'function', + }, + { + inputs: [ + { + components: [ + { + components: [ + { + internalType: 'address', + name: 'offerer', + type: 'address', + }, + { + internalType: 'address', + name: 'zone', + type: 'address', + }, + { + components: [ + { + internalType: 'enum ItemType', + name: 'itemType', + type: 'uint8', + }, + { internalType: 'address', name: 'token', type: 'address' }, + { + internalType: 'uint256', + name: 'identifierOrCriteria', + type: 'uint256', + }, + { internalType: 'uint256', name: 'startAmount', type: 'uint256' }, + { + internalType: 'uint256', + name: 'endAmount', + type: 'uint256', + }, + ], + internalType: 'struct OfferItem[]', + name: 'offer', + type: 'tuple[]', + }, + { + components: [ + { + internalType: 'enum ItemType', + name: 'itemType', + type: 'uint8', + }, + { internalType: 'address', name: 'token', type: 'address' }, + { + internalType: 'uint256', + name: 'identifierOrCriteria', + type: 'uint256', + }, + { internalType: 'uint256', name: 'startAmount', type: 'uint256' }, + { + internalType: 'uint256', + name: 'endAmount', + type: 'uint256', + }, + { internalType: 'address payable', name: 'recipient', type: 'address' }, + ], + internalType: 'struct ConsiderationItem[]', + name: 'consideration', + type: 'tuple[]', + }, + { internalType: 'enum OrderType', name: 'orderType', type: 'uint8' }, + { + internalType: 'uint256', + name: 'startTime', + type: 'uint256', + }, + { internalType: 'uint256', name: 'endTime', type: 'uint256' }, + { + internalType: 'bytes32', + name: 'zoneHash', + type: 'bytes32', + }, + { internalType: 'uint256', name: 'salt', type: 'uint256' }, + { + internalType: 'bytes32', + name: 'conduitKey', + type: 'bytes32', + }, + { internalType: 'uint256', name: 'totalOriginalConsiderationItems', type: 'uint256' }, + ], + internalType: 'struct OrderParameters', + name: 'parameters', + type: 'tuple', + }, + { internalType: 'uint120', name: 'numerator', type: 'uint120' }, + { + internalType: 'uint120', + name: 'denominator', + type: 'uint120', + }, + { internalType: 'bytes', name: 'signature', type: 'bytes' }, + { + internalType: 'bytes', + name: 'extraData', + type: 'bytes', + }, + ], + internalType: 'struct AdvancedOrder[]', + name: '', + type: 'tuple[]', + }, + { + components: [ + { + internalType: 'uint256', + name: 'orderIndex', + type: 'uint256', + }, + { internalType: 'enum Side', name: 'side', type: 'uint8' }, + { + internalType: 'uint256', + name: 'index', + type: 'uint256', + }, + { internalType: 'uint256', name: 'identifier', type: 'uint256' }, + { + internalType: 'bytes32[]', + name: 'criteriaProof', + type: 'bytes32[]', + }, + ], + internalType: 'struct CriteriaResolver[]', + name: '', + type: 'tuple[]', + }, + { + components: [ + { internalType: 'uint256', name: 'orderIndex', type: 'uint256' }, + { + internalType: 'uint256', + name: 'itemIndex', + type: 'uint256', + }, + ], + internalType: 'struct FulfillmentComponent[][]', + name: '', + type: 'tuple[][]', + }, + { + components: [ + { internalType: 'uint256', name: 'orderIndex', type: 'uint256' }, + { + internalType: 'uint256', + name: 'itemIndex', + type: 'uint256', + }, + ], + internalType: 'struct FulfillmentComponent[][]', + name: '', + type: 'tuple[][]', + }, + { internalType: 'bytes32', name: 'fulfillerConduitKey', type: 'bytes32' }, + { + internalType: 'address', + name: 'recipient', + type: 'address', + }, + { internalType: 'uint256', name: 'maximumFulfilled', type: 'uint256' }, + ], + name: 'fulfillAvailableAdvancedOrders', + outputs: [ + { + internalType: 'bool[]', + name: '', + type: 'bool[]', + }, + { + components: [ + { + components: [ + { + internalType: 'enum ItemType', + name: 'itemType', + type: 'uint8', + }, + { internalType: 'address', name: 'token', type: 'address' }, + { + internalType: 'uint256', + name: 'identifier', + type: 'uint256', + }, + { internalType: 'uint256', name: 'amount', type: 'uint256' }, + { + internalType: 'address payable', + name: 'recipient', + type: 'address', + }, + ], + internalType: 'struct ReceivedItem', + name: 'item', + type: 'tuple', + }, + { internalType: 'address', name: 'offerer', type: 'address' }, + { + internalType: 'bytes32', + name: 'conduitKey', + type: 'bytes32', + }, + ], + internalType: 'struct Execution[]', + name: '', + type: 'tuple[]', + }, + ], + stateMutability: 'payable', + type: 'function', + }, + { + inputs: [ + { + components: [ + { + components: [ + { + internalType: 'address', + name: 'offerer', + type: 'address', + }, + { + internalType: 'address', + name: 'zone', + type: 'address', + }, + { + components: [ + { + internalType: 'enum ItemType', + name: 'itemType', + type: 'uint8', + }, + { internalType: 'address', name: 'token', type: 'address' }, + { + internalType: 'uint256', + name: 'identifierOrCriteria', + type: 'uint256', + }, + { internalType: 'uint256', name: 'startAmount', type: 'uint256' }, + { + internalType: 'uint256', + name: 'endAmount', + type: 'uint256', + }, + ], + internalType: 'struct OfferItem[]', + name: 'offer', + type: 'tuple[]', + }, + { + components: [ + { + internalType: 'enum ItemType', + name: 'itemType', + type: 'uint8', + }, + { internalType: 'address', name: 'token', type: 'address' }, + { + internalType: 'uint256', + name: 'identifierOrCriteria', + type: 'uint256', + }, + { internalType: 'uint256', name: 'startAmount', type: 'uint256' }, + { + internalType: 'uint256', + name: 'endAmount', + type: 'uint256', + }, + { internalType: 'address payable', name: 'recipient', type: 'address' }, + ], + internalType: 'struct ConsiderationItem[]', + name: 'consideration', + type: 'tuple[]', + }, + { internalType: 'enum OrderType', name: 'orderType', type: 'uint8' }, + { + internalType: 'uint256', + name: 'startTime', + type: 'uint256', + }, + { internalType: 'uint256', name: 'endTime', type: 'uint256' }, + { + internalType: 'bytes32', + name: 'zoneHash', + type: 'bytes32', + }, + { internalType: 'uint256', name: 'salt', type: 'uint256' }, + { + internalType: 'bytes32', + name: 'conduitKey', + type: 'bytes32', + }, + { internalType: 'uint256', name: 'totalOriginalConsiderationItems', type: 'uint256' }, + ], + internalType: 'struct OrderParameters', + name: 'parameters', + type: 'tuple', + }, + { internalType: 'bytes', name: 'signature', type: 'bytes' }, + ], + internalType: 'struct Order[]', + name: '', + type: 'tuple[]', + }, + { + components: [ + { internalType: 'uint256', name: 'orderIndex', type: 'uint256' }, + { + internalType: 'uint256', + name: 'itemIndex', + type: 'uint256', + }, + ], + internalType: 'struct FulfillmentComponent[][]', + name: '', + type: 'tuple[][]', + }, + { + components: [ + { internalType: 'uint256', name: 'orderIndex', type: 'uint256' }, + { + internalType: 'uint256', + name: 'itemIndex', + type: 'uint256', + }, + ], + internalType: 'struct FulfillmentComponent[][]', + name: '', + type: 'tuple[][]', + }, + { internalType: 'bytes32', name: 'fulfillerConduitKey', type: 'bytes32' }, + { + internalType: 'uint256', + name: 'maximumFulfilled', + type: 'uint256', + }, + ], + name: 'fulfillAvailableOrders', + outputs: [ + { + internalType: 'bool[]', + name: '', + type: 'bool[]', + }, + { + components: [ + { + components: [ + { + internalType: 'enum ItemType', + name: 'itemType', + type: 'uint8', + }, + { internalType: 'address', name: 'token', type: 'address' }, + { + internalType: 'uint256', + name: 'identifier', + type: 'uint256', + }, + { internalType: 'uint256', name: 'amount', type: 'uint256' }, + { + internalType: 'address payable', + name: 'recipient', + type: 'address', + }, + ], + internalType: 'struct ReceivedItem', + name: 'item', + type: 'tuple', + }, + { internalType: 'address', name: 'offerer', type: 'address' }, + { + internalType: 'bytes32', + name: 'conduitKey', + type: 'bytes32', + }, + ], + internalType: 'struct Execution[]', + name: '', + type: 'tuple[]', + }, + ], + stateMutability: 'payable', + type: 'function', + }, + { + inputs: [ + { + components: [ + { + internalType: 'address', + name: 'considerationToken', + type: 'address', + }, + { + internalType: 'uint256', + name: 'considerationIdentifier', + type: 'uint256', + }, + { + internalType: 'uint256', + name: 'considerationAmount', + type: 'uint256', + }, + { internalType: 'address payable', name: 'offerer', type: 'address' }, + { + internalType: 'address', + name: 'zone', + type: 'address', + }, + { internalType: 'address', name: 'offerToken', type: 'address' }, + { + internalType: 'uint256', + name: 'offerIdentifier', + type: 'uint256', + }, + { + internalType: 'uint256', + name: 'offerAmount', + type: 'uint256', + }, + { + internalType: 'enum BasicOrderType', + name: 'basicOrderType', + type: 'uint8', + }, + { internalType: 'uint256', name: 'startTime', type: 'uint256' }, + { + internalType: 'uint256', + name: 'endTime', + type: 'uint256', + }, + { internalType: 'bytes32', name: 'zoneHash', type: 'bytes32' }, + { + internalType: 'uint256', + name: 'salt', + type: 'uint256', + }, + { internalType: 'bytes32', name: 'offererConduitKey', type: 'bytes32' }, + { + internalType: 'bytes32', + name: 'fulfillerConduitKey', + type: 'bytes32', + }, + { + internalType: 'uint256', + name: 'totalOriginalAdditionalRecipients', + type: 'uint256', + }, + { + components: [ + { + internalType: 'uint256', + name: 'amount', + type: 'uint256', + }, + { internalType: 'address payable', name: 'recipient', type: 'address' }, + ], + internalType: 'struct AdditionalRecipient[]', + name: 'additionalRecipients', + type: 'tuple[]', + }, + { internalType: 'bytes', name: 'signature', type: 'bytes' }, + ], + internalType: 'struct BasicOrderParameters', + name: 'parameters', + type: 'tuple', + }, + ], + name: 'fulfillBasicOrder', + outputs: [{ internalType: 'bool', name: 'fulfilled', type: 'bool' }], + stateMutability: 'payable', + type: 'function', + }, + { + inputs: [ + { + components: [ + { + internalType: 'address', + name: 'considerationToken', + type: 'address', + }, + { + internalType: 'uint256', + name: 'considerationIdentifier', + type: 'uint256', + }, + { + internalType: 'uint256', + name: 'considerationAmount', + type: 'uint256', + }, + { internalType: 'address payable', name: 'offerer', type: 'address' }, + { + internalType: 'address', + name: 'zone', + type: 'address', + }, + { internalType: 'address', name: 'offerToken', type: 'address' }, + { + internalType: 'uint256', + name: 'offerIdentifier', + type: 'uint256', + }, + { + internalType: 'uint256', + name: 'offerAmount', + type: 'uint256', + }, + { + internalType: 'enum BasicOrderType', + name: 'basicOrderType', + type: 'uint8', + }, + { internalType: 'uint256', name: 'startTime', type: 'uint256' }, + { + internalType: 'uint256', + name: 'endTime', + type: 'uint256', + }, + { internalType: 'bytes32', name: 'zoneHash', type: 'bytes32' }, + { + internalType: 'uint256', + name: 'salt', + type: 'uint256', + }, + { internalType: 'bytes32', name: 'offererConduitKey', type: 'bytes32' }, + { + internalType: 'bytes32', + name: 'fulfillerConduitKey', + type: 'bytes32', + }, + { + internalType: 'uint256', + name: 'totalOriginalAdditionalRecipients', + type: 'uint256', + }, + { + components: [ + { + internalType: 'uint256', + name: 'amount', + type: 'uint256', + }, + { internalType: 'address payable', name: 'recipient', type: 'address' }, + ], + internalType: 'struct AdditionalRecipient[]', + name: 'additionalRecipients', + type: 'tuple[]', + }, + { internalType: 'bytes', name: 'signature', type: 'bytes' }, + ], + internalType: 'struct BasicOrderParameters', + name: 'parameters', + type: 'tuple', + }, + ], + name: 'fulfillBasicOrder_efficient_6GL6yc', + outputs: [{ internalType: 'bool', name: 'fulfilled', type: 'bool' }], + stateMutability: 'payable', + type: 'function', + }, + { + inputs: [ + { + components: [ + { + components: [ + { + internalType: 'address', + name: 'offerer', + type: 'address', + }, + { + internalType: 'address', + name: 'zone', + type: 'address', + }, + { + components: [ + { + internalType: 'enum ItemType', + name: 'itemType', + type: 'uint8', + }, + { internalType: 'address', name: 'token', type: 'address' }, + { + internalType: 'uint256', + name: 'identifierOrCriteria', + type: 'uint256', + }, + { internalType: 'uint256', name: 'startAmount', type: 'uint256' }, + { + internalType: 'uint256', + name: 'endAmount', + type: 'uint256', + }, + ], + internalType: 'struct OfferItem[]', + name: 'offer', + type: 'tuple[]', + }, + { + components: [ + { + internalType: 'enum ItemType', + name: 'itemType', + type: 'uint8', + }, + { internalType: 'address', name: 'token', type: 'address' }, + { + internalType: 'uint256', + name: 'identifierOrCriteria', + type: 'uint256', + }, + { internalType: 'uint256', name: 'startAmount', type: 'uint256' }, + { + internalType: 'uint256', + name: 'endAmount', + type: 'uint256', + }, + { internalType: 'address payable', name: 'recipient', type: 'address' }, + ], + internalType: 'struct ConsiderationItem[]', + name: 'consideration', + type: 'tuple[]', + }, + { internalType: 'enum OrderType', name: 'orderType', type: 'uint8' }, + { + internalType: 'uint256', + name: 'startTime', + type: 'uint256', + }, + { internalType: 'uint256', name: 'endTime', type: 'uint256' }, + { + internalType: 'bytes32', + name: 'zoneHash', + type: 'bytes32', + }, + { internalType: 'uint256', name: 'salt', type: 'uint256' }, + { + internalType: 'bytes32', + name: 'conduitKey', + type: 'bytes32', + }, + { internalType: 'uint256', name: 'totalOriginalConsiderationItems', type: 'uint256' }, + ], + internalType: 'struct OrderParameters', + name: 'parameters', + type: 'tuple', + }, + { internalType: 'bytes', name: 'signature', type: 'bytes' }, + ], + internalType: 'struct Order', + name: '', + type: 'tuple', + }, + { internalType: 'bytes32', name: 'fulfillerConduitKey', type: 'bytes32' }, + ], + name: 'fulfillOrder', + outputs: [{ internalType: 'bool', name: 'fulfilled', type: 'bool' }], + stateMutability: 'payable', + type: 'function', + }, + { + inputs: [{ internalType: 'address', name: 'contractOfferer', type: 'address' }], + name: 'getContractOffererNonce', + outputs: [{ internalType: 'uint256', name: 'nonce', type: 'uint256' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [{ internalType: 'address', name: 'offerer', type: 'address' }], + name: 'getCounter', + outputs: [{ internalType: 'uint256', name: 'counter', type: 'uint256' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [ + { + components: [ + { internalType: 'address', name: 'offerer', type: 'address' }, + { + internalType: 'address', + name: 'zone', + type: 'address', + }, + { + components: [ + { + internalType: 'enum ItemType', + name: 'itemType', + type: 'uint8', + }, + { internalType: 'address', name: 'token', type: 'address' }, + { + internalType: 'uint256', + name: 'identifierOrCriteria', + type: 'uint256', + }, + { internalType: 'uint256', name: 'startAmount', type: 'uint256' }, + { + internalType: 'uint256', + name: 'endAmount', + type: 'uint256', + }, + ], + internalType: 'struct OfferItem[]', + name: 'offer', + type: 'tuple[]', + }, + { + components: [ + { + internalType: 'enum ItemType', + name: 'itemType', + type: 'uint8', + }, + { internalType: 'address', name: 'token', type: 'address' }, + { + internalType: 'uint256', + name: 'identifierOrCriteria', + type: 'uint256', + }, + { internalType: 'uint256', name: 'startAmount', type: 'uint256' }, + { + internalType: 'uint256', + name: 'endAmount', + type: 'uint256', + }, + { internalType: 'address payable', name: 'recipient', type: 'address' }, + ], + internalType: 'struct ConsiderationItem[]', + name: 'consideration', + type: 'tuple[]', + }, + { internalType: 'enum OrderType', name: 'orderType', type: 'uint8' }, + { + internalType: 'uint256', + name: 'startTime', + type: 'uint256', + }, + { internalType: 'uint256', name: 'endTime', type: 'uint256' }, + { + internalType: 'bytes32', + name: 'zoneHash', + type: 'bytes32', + }, + { internalType: 'uint256', name: 'salt', type: 'uint256' }, + { + internalType: 'bytes32', + name: 'conduitKey', + type: 'bytes32', + }, + { internalType: 'uint256', name: 'counter', type: 'uint256' }, + ], + internalType: 'struct OrderComponents', + name: '', + type: 'tuple', + }, + ], + name: 'getOrderHash', + outputs: [{ internalType: 'bytes32', name: 'orderHash', type: 'bytes32' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [{ internalType: 'bytes32', name: 'orderHash', type: 'bytes32' }], + name: 'getOrderStatus', + outputs: [ + { internalType: 'bool', name: 'isValidated', type: 'bool' }, + { + internalType: 'bool', + name: 'isCancelled', + type: 'bool', + }, + { internalType: 'uint256', name: 'totalFilled', type: 'uint256' }, + { + internalType: 'uint256', + name: 'totalSize', + type: 'uint256', + }, + ], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [], + name: 'incrementCounter', + outputs: [{ internalType: 'uint256', name: 'newCounter', type: 'uint256' }], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [], + name: 'information', + outputs: [ + { internalType: 'string', name: 'version', type: 'string' }, + { + internalType: 'bytes32', + name: 'domainSeparator', + type: 'bytes32', + }, + { internalType: 'address', name: 'conduitController', type: 'address' }, + ], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [ + { + components: [ + { + components: [ + { + internalType: 'address', + name: 'offerer', + type: 'address', + }, + { + internalType: 'address', + name: 'zone', + type: 'address', + }, + { + components: [ + { + internalType: 'enum ItemType', + name: 'itemType', + type: 'uint8', + }, + { internalType: 'address', name: 'token', type: 'address' }, + { + internalType: 'uint256', + name: 'identifierOrCriteria', + type: 'uint256', + }, + { internalType: 'uint256', name: 'startAmount', type: 'uint256' }, + { + internalType: 'uint256', + name: 'endAmount', + type: 'uint256', + }, + ], + internalType: 'struct OfferItem[]', + name: 'offer', + type: 'tuple[]', + }, + { + components: [ + { + internalType: 'enum ItemType', + name: 'itemType', + type: 'uint8', + }, + { internalType: 'address', name: 'token', type: 'address' }, + { + internalType: 'uint256', + name: 'identifierOrCriteria', + type: 'uint256', + }, + { internalType: 'uint256', name: 'startAmount', type: 'uint256' }, + { + internalType: 'uint256', + name: 'endAmount', + type: 'uint256', + }, + { internalType: 'address payable', name: 'recipient', type: 'address' }, + ], + internalType: 'struct ConsiderationItem[]', + name: 'consideration', + type: 'tuple[]', + }, + { internalType: 'enum OrderType', name: 'orderType', type: 'uint8' }, + { + internalType: 'uint256', + name: 'startTime', + type: 'uint256', + }, + { internalType: 'uint256', name: 'endTime', type: 'uint256' }, + { + internalType: 'bytes32', + name: 'zoneHash', + type: 'bytes32', + }, + { internalType: 'uint256', name: 'salt', type: 'uint256' }, + { + internalType: 'bytes32', + name: 'conduitKey', + type: 'bytes32', + }, + { internalType: 'uint256', name: 'totalOriginalConsiderationItems', type: 'uint256' }, + ], + internalType: 'struct OrderParameters', + name: 'parameters', + type: 'tuple', + }, + { internalType: 'uint120', name: 'numerator', type: 'uint120' }, + { + internalType: 'uint120', + name: 'denominator', + type: 'uint120', + }, + { internalType: 'bytes', name: 'signature', type: 'bytes' }, + { + internalType: 'bytes', + name: 'extraData', + type: 'bytes', + }, + ], + internalType: 'struct AdvancedOrder[]', + name: '', + type: 'tuple[]', + }, + { + components: [ + { + internalType: 'uint256', + name: 'orderIndex', + type: 'uint256', + }, + { internalType: 'enum Side', name: 'side', type: 'uint8' }, + { + internalType: 'uint256', + name: 'index', + type: 'uint256', + }, + { internalType: 'uint256', name: 'identifier', type: 'uint256' }, + { + internalType: 'bytes32[]', + name: 'criteriaProof', + type: 'bytes32[]', + }, + ], + internalType: 'struct CriteriaResolver[]', + name: '', + type: 'tuple[]', + }, + { + components: [ + { + components: [ + { + internalType: 'uint256', + name: 'orderIndex', + type: 'uint256', + }, + { internalType: 'uint256', name: 'itemIndex', type: 'uint256' }, + ], + internalType: 'struct FulfillmentComponent[]', + name: 'offerComponents', + type: 'tuple[]', + }, + { + components: [ + { + internalType: 'uint256', + name: 'orderIndex', + type: 'uint256', + }, + { internalType: 'uint256', name: 'itemIndex', type: 'uint256' }, + ], + internalType: 'struct FulfillmentComponent[]', + name: 'considerationComponents', + type: 'tuple[]', + }, + ], + internalType: 'struct Fulfillment[]', + name: '', + type: 'tuple[]', + }, + { internalType: 'address', name: 'recipient', type: 'address' }, + ], + name: 'matchAdvancedOrders', + outputs: [ + { + components: [ + { + components: [ + { + internalType: 'enum ItemType', + name: 'itemType', + type: 'uint8', + }, + { internalType: 'address', name: 'token', type: 'address' }, + { + internalType: 'uint256', + name: 'identifier', + type: 'uint256', + }, + { internalType: 'uint256', name: 'amount', type: 'uint256' }, + { + internalType: 'address payable', + name: 'recipient', + type: 'address', + }, + ], + internalType: 'struct ReceivedItem', + name: 'item', + type: 'tuple', + }, + { internalType: 'address', name: 'offerer', type: 'address' }, + { + internalType: 'bytes32', + name: 'conduitKey', + type: 'bytes32', + }, + ], + internalType: 'struct Execution[]', + name: '', + type: 'tuple[]', + }, + ], + stateMutability: 'payable', + type: 'function', + }, + { + inputs: [ + { + components: [ + { + components: [ + { + internalType: 'address', + name: 'offerer', + type: 'address', + }, + { + internalType: 'address', + name: 'zone', + type: 'address', + }, + { + components: [ + { + internalType: 'enum ItemType', + name: 'itemType', + type: 'uint8', + }, + { internalType: 'address', name: 'token', type: 'address' }, + { + internalType: 'uint256', + name: 'identifierOrCriteria', + type: 'uint256', + }, + { internalType: 'uint256', name: 'startAmount', type: 'uint256' }, + { + internalType: 'uint256', + name: 'endAmount', + type: 'uint256', + }, + ], + internalType: 'struct OfferItem[]', + name: 'offer', + type: 'tuple[]', + }, + { + components: [ + { + internalType: 'enum ItemType', + name: 'itemType', + type: 'uint8', + }, + { internalType: 'address', name: 'token', type: 'address' }, + { + internalType: 'uint256', + name: 'identifierOrCriteria', + type: 'uint256', + }, + { internalType: 'uint256', name: 'startAmount', type: 'uint256' }, + { + internalType: 'uint256', + name: 'endAmount', + type: 'uint256', + }, + { internalType: 'address payable', name: 'recipient', type: 'address' }, + ], + internalType: 'struct ConsiderationItem[]', + name: 'consideration', + type: 'tuple[]', + }, + { internalType: 'enum OrderType', name: 'orderType', type: 'uint8' }, + { + internalType: 'uint256', + name: 'startTime', + type: 'uint256', + }, + { internalType: 'uint256', name: 'endTime', type: 'uint256' }, + { + internalType: 'bytes32', + name: 'zoneHash', + type: 'bytes32', + }, + { internalType: 'uint256', name: 'salt', type: 'uint256' }, + { + internalType: 'bytes32', + name: 'conduitKey', + type: 'bytes32', + }, + { internalType: 'uint256', name: 'totalOriginalConsiderationItems', type: 'uint256' }, + ], + internalType: 'struct OrderParameters', + name: 'parameters', + type: 'tuple', + }, + { internalType: 'bytes', name: 'signature', type: 'bytes' }, + ], + internalType: 'struct Order[]', + name: '', + type: 'tuple[]', + }, + { + components: [ + { + components: [ + { + internalType: 'uint256', + name: 'orderIndex', + type: 'uint256', + }, + { internalType: 'uint256', name: 'itemIndex', type: 'uint256' }, + ], + internalType: 'struct FulfillmentComponent[]', + name: 'offerComponents', + type: 'tuple[]', + }, + { + components: [ + { + internalType: 'uint256', + name: 'orderIndex', + type: 'uint256', + }, + { internalType: 'uint256', name: 'itemIndex', type: 'uint256' }, + ], + internalType: 'struct FulfillmentComponent[]', + name: 'considerationComponents', + type: 'tuple[]', + }, + ], + internalType: 'struct Fulfillment[]', + name: '', + type: 'tuple[]', + }, + ], + name: 'matchOrders', + outputs: [ + { + components: [ + { + components: [ + { + internalType: 'enum ItemType', + name: 'itemType', + type: 'uint8', + }, + { internalType: 'address', name: 'token', type: 'address' }, + { + internalType: 'uint256', + name: 'identifier', + type: 'uint256', + }, + { internalType: 'uint256', name: 'amount', type: 'uint256' }, + { + internalType: 'address payable', + name: 'recipient', + type: 'address', + }, + ], + internalType: 'struct ReceivedItem', + name: 'item', + type: 'tuple', + }, + { internalType: 'address', name: 'offerer', type: 'address' }, + { + internalType: 'bytes32', + name: 'conduitKey', + type: 'bytes32', + }, + ], + internalType: 'struct Execution[]', + name: '', + type: 'tuple[]', + }, + ], + stateMutability: 'payable', + type: 'function', + }, + { + inputs: [], + name: 'name', + outputs: [{ internalType: 'string', name: '', type: 'string' }], + stateMutability: 'pure', + type: 'function', + }, + { + inputs: [ + { + components: [ + { + components: [ + { + internalType: 'address', + name: 'offerer', + type: 'address', + }, + { + internalType: 'address', + name: 'zone', + type: 'address', + }, + { + components: [ + { + internalType: 'enum ItemType', + name: 'itemType', + type: 'uint8', + }, + { internalType: 'address', name: 'token', type: 'address' }, + { + internalType: 'uint256', + name: 'identifierOrCriteria', + type: 'uint256', + }, + { internalType: 'uint256', name: 'startAmount', type: 'uint256' }, + { + internalType: 'uint256', + name: 'endAmount', + type: 'uint256', + }, + ], + internalType: 'struct OfferItem[]', + name: 'offer', + type: 'tuple[]', + }, + { + components: [ + { + internalType: 'enum ItemType', + name: 'itemType', + type: 'uint8', + }, + { internalType: 'address', name: 'token', type: 'address' }, + { + internalType: 'uint256', + name: 'identifierOrCriteria', + type: 'uint256', + }, + { internalType: 'uint256', name: 'startAmount', type: 'uint256' }, + { + internalType: 'uint256', + name: 'endAmount', + type: 'uint256', + }, + { internalType: 'address payable', name: 'recipient', type: 'address' }, + ], + internalType: 'struct ConsiderationItem[]', + name: 'consideration', + type: 'tuple[]', + }, + { internalType: 'enum OrderType', name: 'orderType', type: 'uint8' }, + { + internalType: 'uint256', + name: 'startTime', + type: 'uint256', + }, + { internalType: 'uint256', name: 'endTime', type: 'uint256' }, + { + internalType: 'bytes32', + name: 'zoneHash', + type: 'bytes32', + }, + { internalType: 'uint256', name: 'salt', type: 'uint256' }, + { + internalType: 'bytes32', + name: 'conduitKey', + type: 'bytes32', + }, + { internalType: 'uint256', name: 'totalOriginalConsiderationItems', type: 'uint256' }, + ], + internalType: 'struct OrderParameters', + name: 'parameters', + type: 'tuple', + }, + { internalType: 'bytes', name: 'signature', type: 'bytes' }, + ], + internalType: 'struct Order[]', + name: '', + type: 'tuple[]', + }, + ], + name: 'validate', + outputs: [{ internalType: 'bool', name: '', type: 'bool' }], + stateMutability: 'nonpayable', + type: 'function', + }, + { stateMutability: 'payable', type: 'receive' }, +] as const; diff --git a/packages/datasources/src/lib/types.ts b/packages/datasources/src/lib/types.ts index 93fc7207e..bc2484122 100644 --- a/packages/datasources/src/lib/types.ts +++ b/packages/datasources/src/lib/types.ts @@ -55,6 +55,7 @@ export const DatasourceNames = { ENSRoot: "ensroot", Basenames: "basenames", Lineanames: "lineanames", + Seaport: "seaport", ThreeDNSOptimism: "threedns-optimism", ThreeDNSBase: "threedns-base", } as const; diff --git a/packages/datasources/src/mainnet.ts b/packages/datasources/src/mainnet.ts index 09fa94573..19773b711 100644 --- a/packages/datasources/src/mainnet.ts +++ b/packages/datasources/src/mainnet.ts @@ -23,6 +23,9 @@ import { Registry as linea_Registry } from "./abis/lineanames/Registry"; import { ThreeDNSToken } from "./abis/threedns/ThreeDNSToken"; import { ResolverConfig } from "./lib/resolver"; +// ABIs for Seaport Datasource +import { Seaport } from "./abis/seaport/Seaport"; + /** * The Mainnet ENSNamespace */ @@ -216,4 +219,15 @@ export default { }, }, }, + + [DatasourceNames.Seaport]: { + chain: mainnet, + contracts: { + Seaport: { + abi: Seaport, + address: "0x00000000000000ADc04C56Bf30aC9d3c0aAF14dC", + startBlock: 17129405, + }, + }, + } } satisfies ENSNamespace; diff --git a/packages/datasources/src/sepolia.ts b/packages/datasources/src/sepolia.ts index 9b2deb2f9..86a9f2853 100644 --- a/packages/datasources/src/sepolia.ts +++ b/packages/datasources/src/sepolia.ts @@ -1,4 +1,4 @@ -import { baseSepolia, lineaSepolia, sepolia } from "viem/chains"; +import {base, baseSepolia, lineaSepolia, sepolia} from "viem/chains"; import { ResolverConfig } from "./lib/resolver"; import { DatasourceNames, type ENSNamespace } from "./lib/types"; @@ -21,6 +21,7 @@ import { BaseRegistrar as linea_BaseRegistrar } from "./abis/lineanames/BaseRegi import { EthRegistrarController as linea_EthRegistrarController } from "./abis/lineanames/EthRegistrarController"; import { NameWrapper as linea_NameWrapper } from "./abis/lineanames/NameWrapper"; import { Registry as linea_Registry } from "./abis/lineanames/Registry"; +import {Seaport} from "./abis/seaport/Seaport"; /** * The Sepolia ENSNamespace @@ -173,4 +174,15 @@ export default { }, }, }, + + [DatasourceNames.Seaport]: { + chain: sepolia, + contracts: { + Seaport: { + abi: Seaport, + address: "0x00000000000000ADc04C56Bf30aC9d3c0aAF14dC", + startBlock: 3365529, + }, + }, + } } satisfies ENSNamespace; diff --git a/packages/ensnode-schema/src/ponder.schema.ts b/packages/ensnode-schema/src/ponder.schema.ts index 4657c863f..b1f0eec24 100644 --- a/packages/ensnode-schema/src/ponder.schema.ts +++ b/packages/ensnode-schema/src/ponder.schema.ts @@ -3,3 +3,4 @@ */ export * from "./subgraph.schema"; export * from "./resolver-records.schema"; +export * from "./seaport.schema"; diff --git a/packages/ensnode-schema/src/seaport.schema.ts b/packages/ensnode-schema/src/seaport.schema.ts new file mode 100644 index 000000000..e2803d199 --- /dev/null +++ b/packages/ensnode-schema/src/seaport.schema.ts @@ -0,0 +1,35 @@ +import {index, onchainTable} from "ponder"; + +const sharedEventColumns = (t: any) => ({ + id: t.text().primaryKey(), + blockNumber: t.integer().notNull(), + transactionID: t.hex().notNull(), +}); + +export const currency = onchainTable("currency", (t) => ({ + id: t.hex().primaryKey(), + name: t.text(), + symbol: t.text(), + decimals: t.integer().notNull(), + contractAddress: t.hex().notNull(), + chainId: t.integer().notNull(), +})); + +export const nameSold = onchainTable( + "name_sold", + (t) => ({ + ...sharedEventColumns(t), + fromOwnerId: t.hex().notNull(), + newOwnerId: t.hex().notNull(), + currencyId: t.hex().notNull(), + price: t.bigint().notNull(), + chainId: t.integer().notNull(), + orderHash: t.hex().notNull(), + }), + (t) => ({ + idx_from: index().on(t.fromOwnerId), + idx_to: index().on(t.newOwnerId), + idx_currency: index().on(t.currencyId), + idx_compound: index().on(t.fromOwnerId, t.id), + }), +); diff --git a/packages/ensnode-sdk/src/utils/types.ts b/packages/ensnode-sdk/src/utils/types.ts index ca8ef986d..7820eaa6d 100644 --- a/packages/ensnode-sdk/src/utils/types.ts +++ b/packages/ensnode-sdk/src/utils/types.ts @@ -9,6 +9,7 @@ export enum PluginName { Basenames = "basenames", Lineanames = "lineanames", ThreeDNS = "threedns", + Seaport = "seaport", } /** diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 15105b346..6693fc402 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -273,6 +273,9 @@ importers: '@ensnode/ponder-subgraph': specifier: workspace:* version: link:../../packages/ponder-subgraph + '@opensea/seaport-js': + specifier: ^4.0.5 + version: 4.0.5(bufferutil@4.0.9)(utf-8-validate@5.0.10) '@types/dns-packet': specifier: ^5.6.5 version: 5.6.5 @@ -1839,6 +1842,9 @@ packages: resolution: {integrity: sha512-rONPWMC7PeExE077uLE4oqWrZ1IvAfz3oH9LibVAcVCopJiA9R62uavnbEzdkVmJYI6M6Zgkbeb07+tWjlq2XA==} engines: {node: ^14.21.3 || >=16} + '@noble/curves@1.2.0': + resolution: {integrity: sha512-oYclrNgRaM9SsBUBVbb8M6DTV7ZHRTKugureoYEncY5c65HOmRzvSiTE3y5CYaPYJA/GVkrhXEoF0M3Ya9PMnw==} + '@noble/curves@1.4.0': resolution: {integrity: sha512-p+4cb332SFCrReJkCYe8Xzm0OWi4Jji5jVdIZRL/PmacmDkFNw6MrrV+gGpiPxLHbV+zKFRywUWbaseT+tZRXg==} @@ -1853,6 +1859,10 @@ packages: resolution: {integrity: sha512-warwspo+UYUPep0Q+vtdVB4Ugn8GGQj8iyB3gnRWsztmUHTI3S1nhdiWNsPUGL0vud7JlRRk1XEu7Lq1KGTnMQ==} engines: {node: ^14.21.3 || >=16} + '@noble/hashes@1.3.2': + resolution: {integrity: sha512-MVC8EAQp7MvEcm30KWENFjgR+Mkmf+D189XJTkFIlwohU5hcBbn1ZkKq7KVTi2Hme3PMGF390DaL52beVrIihQ==} + engines: {node: '>= 16'} + '@noble/hashes@1.4.0': resolution: {integrity: sha512-V1JJ1WTRUqHHrOSh597hURcMqVKVGL/ea3kv0gSnEdsEZ0/+VyPghM1lMNGc00z7CIQorSvbKpuJkxvuHbvdbg==} engines: {node: '>= 16'} @@ -1937,6 +1947,10 @@ packages: '@octokit/types@13.10.0': resolution: {integrity: sha512-ifLaO34EbbPj0Xgro4G5lP5asESjwHracYJvVaPIyXMuiuXLlhic3S47cBdTb+jfODkTE5YtGCLt3Ay3+J97sA==} + '@opensea/seaport-js@4.0.5': + resolution: {integrity: sha512-hyJEHSCFmO7kv2G+ima0kCpt0kvLa6QOSHb1HJuLd8DS3bao0gOa/Q3AhM3xUqO6SZZ8aD9njhu1EDqjC/5pOw==} + engines: {node: '>=20.0.0'} + '@opentelemetry/api@1.7.0': resolution: {integrity: sha512-AdY5wvN0P2vXBi3b29hxZgSFvdhdxPB9+f0B6s//P9Q8nibRWeA3cHm8UmLpio9ABigkVHJ5NMPk+Mz8VCCyrw==} engines: {node: '>=8.0.0'} @@ -2818,6 +2832,9 @@ packages: '@types/node@22.15.3': resolution: {integrity: sha512-lX7HFZeHf4QG/J7tBZqrCAXwz9J5RD56Y6MpP0eJkka8p+K0RY/yBTW7CYFJ4VGCclxqOLKmiGP5juQc6MKgcw==} + '@types/node@22.7.5': + resolution: {integrity: sha512-jML7s2NAzMWc//QSJ1a3prpk78cOPchGvXJsC3C6R6PSMoooztvRVQEz89gmBTBY1SPMaqo5teB4uNHPdetShQ==} + '@types/progress@2.0.7': resolution: {integrity: sha512-iadjw02vte8qWx7U0YM++EybBha2CQLPGu9iJ97whVgJUT5Zq9MjAPYUnbfRI2Kpehimf1QjFJYxD0t8nqzu5w==} @@ -3045,6 +3062,7 @@ packages: '@walletconnect/modal@2.7.0': resolution: {integrity: sha512-RQVt58oJ+rwqnPcIvRFeMGKuXb9qkgSmwz4noF8JZGUym3gUAzVs+uW2NQ1Owm9XOJAV+sANrtJ+VoVq1ftElw==} + deprecated: Please follow the migration guide on https://docs.reown.com/appkit/upgrade/wcm '@walletconnect/relay-api@1.0.11': resolution: {integrity: sha512-tLPErkze/HmC9aCmdZOhtVmYZq1wKfWTJtygQHoWtgg722Jd4homo54Cs4ak2RUFUZIGO2RsOpIcWipaua5D5Q==} @@ -3168,6 +3186,9 @@ packages: engines: {node: '>=0.4.0'} hasBin: true + aes-js@4.0.0-beta.5: + resolution: {integrity: sha512-G965FqalsNyrPqgEGON7nIx1e/OVENSgiEIzyC63haUMuvNnwIgIjMs52hlTCKhkBny7A2ORNlfY9Zu+jmGk1Q==} + agentkeepalive@4.6.0: resolution: {integrity: sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ==} engines: {node: '>= 8.0.0'} @@ -3418,6 +3439,9 @@ packages: engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} hasBin: true + buffer-reverse@1.0.1: + resolution: {integrity: sha512-M87YIUBsZ6N924W57vDwT/aOu8hw7ZgdByz6ijksLjmHJELBASmYTTlNHRgjE+pTsT9oJXGaDSgqqwfdHotDUg==} + buffer-writer@2.0.0: resolution: {integrity: sha512-a7ZpuTZU1TRtnwyCNW3I5dc0wWNC3VR9S++Ewyk2HHZdrO3CQJqSpd+95Us590V6AL7JqUAH2IwZ/398PmNFgw==} engines: {node: '>=4'} @@ -3668,6 +3692,9 @@ packages: crossws@0.3.3: resolution: {integrity: sha512-/71DJT3xJlqSnBr83uGJesmVHSzZEvgxHt/fIKxBAAngqMHmnBWQNxCphVxxJ2XL3xleu5+hJD6IQ3TglBedcw==} + crypto-js@4.2.0: + resolution: {integrity: sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q==} + css-selector-parser@3.0.5: resolution: {integrity: sha512-3itoDFbKUNx1eKmVpYMFyqKX04Ww9osZ+dLgrk6GEv6KMVeXUhUnp4I5X+evw+u3ZxVU6RFXSSRxlTeMh8bA+g==} @@ -4303,6 +4330,10 @@ packages: ethereum-cryptography@2.2.1: resolution: {integrity: sha512-r/W8lkHSiTLxUxW8Rf3u4HGB0xQweG2RyETjywylKZSzLWoWAijRz8WCuOtJ6wah+avllXBqZuk29HCCvhEIRg==} + ethers@6.15.0: + resolution: {integrity: sha512-Kf/3ZW54L4UT0pZtsY/rf+EkBU7Qi5nnhonjUb8yTXcxH3cdcWrV2cRyk0Xk/4jK6OoHhxxZHriyhje20If2hQ==} + engines: {node: '>=14.0.0'} + event-target-shim@5.0.1: resolution: {integrity: sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==} engines: {node: '>=6'} @@ -5386,6 +5417,10 @@ packages: resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} engines: {node: '>= 8'} + merkletreejs@0.5.2: + resolution: {integrity: sha512-MHqclSWRSQQbYciUMALC3PZmE23NPf5IIYo+Z7qAz5jVcqgCB95L1T9jGcr+FtOj2Pa2/X26uG2Xzxs7FJccUg==} + engines: {node: '>= 7.6.0'} + meros@1.3.0: resolution: {integrity: sha512-2BNGOimxEz5hmjUG2FwoxCt5HN7BXdaWyFqEwxPTrJzVdABtrL4TiHTcsWSFAxPQ/tOnEaQEJh3qWq71QRMY+w==} engines: {node: '>=13'} @@ -6889,6 +6924,10 @@ packages: resolution: {integrity: sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==} hasBin: true + treeify@1.1.0: + resolution: {integrity: sha512-1m4RA7xVAJrSGrrXGs0L3YTwyvBs2S8PbRHaLZAkFw7JR8oIFwYtysxlBZhYIa7xSyiYJKZ3iGrrk55cGA3i9A==} + engines: {node: '>=0.6'} + trim-lines@3.0.1: resolution: {integrity: sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==} @@ -6940,6 +6979,9 @@ packages: tslib@1.14.1: resolution: {integrity: sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==} + tslib@2.7.0: + resolution: {integrity: sha512-gLXCKdN1/j47AiHiOkJN69hJmcbGTHI0ImLmbYLHykhgeN0jVGola9yVjFgzCUklsZQMW55o+dW7IXv3RCXDzA==} + tslib@2.8.1: resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} @@ -9397,6 +9439,10 @@ snapshots: '@noble/ciphers@1.2.1': {} + '@noble/curves@1.2.0': + dependencies: + '@noble/hashes': 1.3.2 + '@noble/curves@1.4.0': dependencies: '@noble/hashes': 1.4.0 @@ -9413,6 +9459,8 @@ snapshots: dependencies: '@noble/hashes': 1.7.1 + '@noble/hashes@1.3.2': {} + '@noble/hashes@1.4.0': {} '@noble/hashes@1.5.0': {} @@ -9498,6 +9546,14 @@ snapshots: dependencies: '@octokit/openapi-types': 24.2.0 + '@opensea/seaport-js@4.0.5(bufferutil@4.0.9)(utf-8-validate@5.0.10)': + dependencies: + ethers: 6.15.0(bufferutil@4.0.9)(utf-8-validate@5.0.10) + merkletreejs: 0.5.2 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + '@opentelemetry/api@1.7.0': {} '@oslojs/encoding@1.1.0': {} @@ -10370,6 +10426,10 @@ snapshots: dependencies: undici-types: 6.21.0 + '@types/node@22.7.5': + dependencies: + undici-types: 6.19.8 + '@types/progress@2.0.7': dependencies: '@types/node': 22.15.3 @@ -11130,6 +11190,8 @@ snapshots: acorn@8.14.1: {} + aes-js@4.0.0-beta.5: {} + agentkeepalive@4.6.0: dependencies: humanize-ms: 1.2.1 @@ -11491,6 +11553,8 @@ snapshots: node-releases: 2.0.19 update-browserslist-db: 1.1.2(browserslist@4.24.4) + buffer-reverse@1.0.1: {} + buffer-writer@2.0.0: {} buffer@6.0.3: @@ -11731,6 +11795,8 @@ snapshots: dependencies: uncrypto: 0.1.3 + crypto-js@4.2.0: {} + css-selector-parser@3.0.5: {} css-tree@3.1.0: @@ -12438,6 +12504,19 @@ snapshots: '@scure/bip32': 1.4.0 '@scure/bip39': 1.3.0 + ethers@6.15.0(bufferutil@4.0.9)(utf-8-validate@5.0.10): + dependencies: + '@adraffy/ens-normalize': 1.10.1 + '@noble/curves': 1.2.0 + '@noble/hashes': 1.3.2 + '@types/node': 22.7.5 + aes-js: 4.0.0-beta.5 + tslib: 2.7.0 + ws: 8.17.1(bufferutil@4.0.9)(utf-8-validate@5.0.10) + transitivePeerDependencies: + - bufferutil + - utf-8-validate + event-target-shim@5.0.1: {} eventemitter2@6.4.9: {} @@ -13778,6 +13857,12 @@ snapshots: merge2@1.4.1: {} + merkletreejs@0.5.2: + dependencies: + buffer-reverse: 1.0.1 + crypto-js: 4.2.0 + treeify: 1.1.0 + meros@1.3.0(@types/node@22.15.3): optionalDependencies: '@types/node': 22.15.3 @@ -15710,6 +15795,8 @@ snapshots: tree-kill@1.2.2: {} + treeify@1.1.0: {} + trim-lines@3.0.1: {} trim-trailing-lines@2.1.0: {} @@ -15756,6 +15843,8 @@ snapshots: tslib@1.14.1: {} + tslib@2.7.0: {} + tslib@2.8.1: {} tsup@8.3.6(jiti@2.4.2)(postcss@8.5.3)(tsx@4.19.3)(typescript@5.7.3)(yaml@2.7.0): From f8433f1a17f3e35a869840f0490edd285f887b6a Mon Sep 17 00:00:00 2001 From: Zimtente Date: Mon, 4 Aug 2025 14:55:40 +0200 Subject: [PATCH 02/61] docs(changeset): Added new Plugin: TokenScope. This Plugin for now will index Seaport-Sales across all other name-plugins we support (ENS, 3dns etc) --- .changeset/six-pillows-pump.md | 7 +++++++ apps/ensindexer/src/plugins/index.ts | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) create mode 100644 .changeset/six-pillows-pump.md diff --git a/.changeset/six-pillows-pump.md b/.changeset/six-pillows-pump.md new file mode 100644 index 000000000..5fa8971d8 --- /dev/null +++ b/.changeset/six-pillows-pump.md @@ -0,0 +1,7 @@ +--- +"@ensnode/ensnode-schema": minor +"@ensnode/datasources": minor +"ensindexer": minor +--- + +Added new Plugin: TokenScope. This Plugin for now will index Seaport-Sales across all other name-plugins we support (ENS, 3dns etc) diff --git a/apps/ensindexer/src/plugins/index.ts b/apps/ensindexer/src/plugins/index.ts index 14d604bbb..bec622776 100644 --- a/apps/ensindexer/src/plugins/index.ts +++ b/apps/ensindexer/src/plugins/index.ts @@ -5,9 +5,9 @@ import basenamesPlugin from "./basenames/plugin"; import lineaNamesPlugin from "./lineanames/plugin"; import referralsPlugin from "./referrals/plugin"; import reverseResolversPlugin from "./reverse-resolvers/plugin"; -import tokenScopePlugin from "./tokenscope/plugin"; import subgraphPlugin from "./subgraph/plugin"; import threednsPlugin from "./threedns/plugin"; +import tokenScopePlugin from "./tokenscope/plugin"; export const ALL_PLUGINS = [ subgraphPlugin, From 0c4485b1855a2a2c60ad1346ce5567e8e0d2895c Mon Sep 17 00:00:00 2001 From: Zimtente Date: Mon, 11 Aug 2025 15:44:45 +0200 Subject: [PATCH 03/61] working on review items --- apps/ensindexer/src/handlers/Seaport.ts | 531 +++++++++++------- .../UnwrappedEthRegistrarController.ts | 5 +- packages/datasources/src/index.ts | 398 ++++++------- .../ensnode-schema/src/tokenscope.schema.ts | 145 ++--- packages/ensnode-sdk/src/ens/constants.ts | 3 + packages/ensnode-sdk/src/ens/index.ts | 1 + packages/ensnode-sdk/src/index.ts | 1 + 7 files changed, 610 insertions(+), 474 deletions(-) create mode 100644 packages/ensnode-sdk/src/ens/constants.ts create mode 100644 packages/ensnode-sdk/src/ens/index.ts diff --git a/apps/ensindexer/src/handlers/Seaport.ts b/apps/ensindexer/src/handlers/Seaport.ts index 6891304a0..2bc865b6a 100644 --- a/apps/ensindexer/src/handlers/Seaport.ts +++ b/apps/ensindexer/src/handlers/Seaport.ts @@ -1,205 +1,267 @@ -import { Context } from "ponder:registry"; +import {Context} from "ponder:registry"; import schema from "ponder:schema"; -import { ItemType } from "@opensea/seaport-js/lib/constants"; +import {ItemType} from "@opensea/seaport-js/lib/constants"; import config from "@/config"; -import { sharedEventValues, upsertAccount } from "@/lib/db-helpers"; -import { EventWithArgs } from "@/lib/ponder-helpers"; -import { getDomainIdByTokenId, isKnownTokenIssuingContract } from "@ensnode/datasources"; -import { Address, Hex } from "viem"; +import {sharedEventValues, upsertAccount} from "@/lib/db-helpers"; +import {EventWithArgs} from "@/lib/ponder-helpers"; +import {getDomainIdByTokenId, isKnownTokenIssuingContract} from "@ensnode/datasources"; +import {Address, Hex} from "viem"; +import {uint256ToHex32} from "@ensnode/ensnode-sdk"; type OfferItem = { - itemType: ItemType; - token: Address; // contract address - identifier: bigint; // token id - amount: bigint; -}; + /** + * The type of item in the offer. + * For example, ERC20, ERC721, ERC1155, or NATIVE (ETH) + */ + itemType: ItemType; -type ConsiderationItem = { - itemType: ItemType; - token: Address; // contract address - identifier: bigint; // token id - amount: bigint; - recipient: Address; -}; + /** + * The contract address of the token. + * - For ERC721/ERC1155: The NFT contract address + * - For ERC20: The token contract address + * - For NATIVE (ETH): Zero address (0x0000000000000000000000000000000000000000) + */ + token: Address; -type Item = OfferItem | ConsiderationItem; + /** + * The identifier field has different meanings based on itemType: + * - For ERC721/ERC1155: The specific token ID of the NFT + * - For ERC20: Always 0 (not used for fungible tokens) + * - For NATIVE (ETH): Always 0 (not used for native currency) + */ + identifier: bigint; -interface SeaportOrderFulfilledEvent - extends EventWithArgs<{ /** - * The unique hash identifier of the fulfilled order. - * Used to track and reference specific orders on-chain. + * The amount field has different meanings based on itemType: + * - For ERC721: Always 1 (you can only transfer 1 unique NFT) + * - For ERC1155: The quantity of tokens with the specified identifier (for our purposes, always 1) + * - For ERC20: The amount of tokens (in wei/smallest unit) + * - For NATIVE (ETH): The amount of ETH (in wei) */ - orderHash: Hex; + amount: bigint; +}; +type ConsiderationItem = { /** - * The address of the account that created and signed the original order. - * This is the party offering items for trade. + * The type of item in the consideration. + * For example, ERC20, ERC721, ERC1155, or NATIVE (ETH) */ - offerer: Address; + itemType: ItemType; /** - * The address of the zone contract that implements custom validation rules. - * Zones can enforce additional restrictions like allowlists, time windows, - * or other custom logic before order fulfillment. Can be zero address if - * no additional validation is required. + * The contract address of the token. + * - For ERC721/ERC1155: The NFT contract address + * - For ERC20: The token contract address + * - For NATIVE (ETH): Zero address (0x0000000000000000000000000000000000000000) */ - zone: Address; + token: Address; /** - * The address that receives the offered items from the order. - * This is typically the order fulfiller or their designated recipient. + * The identifier field has different meanings based on itemType: + * - For ERC721/ERC1155: The specific token ID of the NFT + * - For ERC20: Always 0 (not used for fungible tokens) + * - For NATIVE (ETH): Always 0 (not used for native currency) */ - recipient: Address; + identifier: bigint; /** - * Array of items that the offerer is giving up in this order. - * For listings: NFTs/tokens being sold - * For offers: ETH/ERC20 tokens being offered as payment + * The amount field has different meanings based on itemType: + * - For ERC721: Always 1 (you can only transfer 1 unique NFT) + * - For ERC1155: The quantity of tokens with the specified identifier + * - For ERC20: The amount of tokens (in wei/smallest unit) + * - For NATIVE (ETH): The amount of ETH (in wei) */ - offer: readonly OfferItem[]; + amount: bigint; /** - * Array of items that the offerer expects to receive in return. - * For listings: ETH/ERC20 tokens expected as payment - * For offers: NFTs/tokens being requested in exchange + * The address that receives the consideration items from the order. + * This is typically the order fulfiller or their designated recipient. */ - consideration: readonly ConsiderationItem[]; - }> {} + recipient: Address; +}; + +type Item = OfferItem | ConsiderationItem; + +interface SeaportOrderFulfilledEvent + extends EventWithArgs<{ + /** + * The unique hash identifier of the fulfilled order. + * Used to track and reference specific orders on-chain. + */ + orderHash: Hex; + + /** + * The address of the account that created and signed the original order. + * This is the party offering items for trade. + */ + offerer: Address; + + /** + * The address of the zone contract that implements custom validation rules. + * Zones can enforce additional restrictions like allowlists, time windows, + * or other custom logic before order fulfillment. Can be zero address if + * no additional validation is required. + */ + zone: Address; + + /** + * The address that receives the offered items from the order. + * This is typically the order fulfiller or their designated recipient. + */ + recipient: Address; + + /** + * Array of items that the offerer is giving up in this order. + * For listings: NFTs/tokens being sold + * For offers: ETH/ERC20 tokens being offered as payment + */ + offer: readonly OfferItem[]; + + /** + * Array of items that the offerer expects to receive in return. + * For listings: ETH/ERC20 tokens expected as payment + * For offers: NFTs/tokens being requested in exchange + */ + consideration: readonly ConsiderationItem[]; + }> { +} type PaymentDetails = { - currencyAddress: Address; - totalAmount: bigint; + currencyAddress: Address; + totalAmount: bigint; }; /** - * Get the payment token address and total amount from the payment items + * Validates and extracts payment details from payment items. + * Returns null if validation fails (no items, mixed currencies, etc.) */ -function validateAndGetPaymentDetails(paymentItems: Item[]): PaymentDetails { - if (paymentItems.length === 0) { - throw new Error( - "No payment item. Provide at least one payment item to get the payment token address.", - ); - } - - // Get all unique tokens used in payment items - const paymentTokens = paymentItems.map((item) => item.token); - const uniqueTokens = [...new Set(paymentTokens)]; - - // Mixed currencies - if (uniqueTokens.length > 1) { - throw new Error( - "Too many currencies used. All payment items must be paid for with exactly the same currency.", - ); - } - - // No currency - if (uniqueTokens.length === 0) { - throw new Error( - "No payment item. Provide at least one payment item to get the payment token address.", - ); - } - - // Calculate total payment amount - const totalAmount = paymentItems.reduce((total, item) => total + item.amount, 0n); - - return { - currencyAddress: uniqueTokens[0]!, - totalAmount, - }; +function validateAndGetPaymentDetails(paymentItems: Item[]): PaymentDetails | null { + // No payment items + if (paymentItems.length === 0) { + return null; + } + + // Get all unique tokens used in payment items + const paymentTokens = paymentItems.map((item) => item.token); + const uniqueTokens = [...new Set(paymentTokens)]; + + // Mixed currencies - not supported + if (uniqueTokens.length > 1) { + return null; + } + + // No currency (shouldn't happen if we have items, but being safe) + if (uniqueTokens.length === 0) { + return null; + } + + // Calculate total payment amount + const totalAmount = paymentItems.reduce((total, item) => total + item.amount, 0n); + + // Validate amount is positive + if (totalAmount <= 0n) { + return null; + } + + return { + currencyAddress: uniqueTokens[0]!, + totalAmount, + }; } /** * Handles NFT offers being fulfilled (seller accepting a buyer's offer) */ async function handleOfferFulfilled( - context: Context, - event: SeaportOrderFulfilledEvent, - nftItem: ConsiderationItem, - paymentItems: OfferItem[], + context: Context, + event: SeaportOrderFulfilledEvent, + nftItem: ConsiderationItem, + paymentItems: OfferItem[], ) { - const { orderHash, offerer, recipient } = event.args; - - // In a fulfilled offer, the offerer is buying the NFT, recipient is selling - const buyer = offerer; - const seller = recipient; - - // Ensure accounts exist - await upsertAccount(context, buyer); - await upsertAccount(context, seller); - - // Get payment details - const { currencyAddress, totalAmount } = validateAndGetPaymentDetails(paymentItems); - - const contractAddress = nftItem.token; - const tokenId = nftItem.identifier.toString(); - - // Get Domain ID - const domainId = getDomainIdByTokenId(config.namespace, contractAddress, tokenId); - - // Record the sale - await context.db.insert(schema.nameSold).values({ - ...sharedEventValues(context.chain.id, event), - fromOwnerId: seller, - newOwnerId: buyer, - currencyAddress: currencyAddress, - chainId: context.chain.id, - logIndex: event.log.logIndex, - orderHash: orderHash, - price: totalAmount, - contractAddress: contractAddress, - tokenId: tokenId, - tokenType: nftItem.itemType === ItemType.ERC721 ? "ERC721" : "ERC1155", - domainId: domainId, - createdAt: event.block.timestamp, - }); + // Get payment details + const paymentDetails = validateAndGetPaymentDetails(paymentItems)!; + + const {orderHash, offerer, recipient} = event.args; + + // In a fulfilled offer, the offerer is buying the NFT, recipient is selling + const buyer = offerer; + const seller = recipient; + + // Ensure accounts exist + await upsertAccount(context, buyer); + await upsertAccount(context, seller); + + const contractAddress = nftItem.token; + const tokenId = nftItem.identifier.toString(); + + // Get Domain ID + const tokenIdHex = uint256ToHex32(BigInt(tokenId)); + const domainId = getDomainIdByTokenId(config.namespace, contractAddress, tokenIdHex); + + // Record the sale + await context.db.insert(schema.nameSold).values({ + ...sharedEventValues(context.chain.id, event), + fromOwnerId: seller, + newOwnerId: buyer, + currencyAddress: paymentDetails.currencyAddress, + chainId: context.chain.id, + logIndex: event.log.logIndex, + orderHash: orderHash, + price: paymentDetails.totalAmount, + contractAddress: contractAddress, + tokenId: tokenId, + tokenType: nftItem.itemType === ItemType.ERC721 ? "ERC721" : "ERC1155", + domainId: domainId, + timestamp: event.block.timestamp, + }); } /** * Handles NFT listings being fulfilled (buyer accepting a seller's listing) */ async function handleListingFulfilled( - context: Context, - event: SeaportOrderFulfilledEvent, - nftItem: OfferItem, - paymentItems: ConsiderationItem[], + context: Context, + event: SeaportOrderFulfilledEvent, + nftItem: OfferItem, + paymentItems: ConsiderationItem[], ) { - const { orderHash, offerer, recipient } = event.args; - - // In a fulfilled listing, the offerer is selling the NFT, recipient is buying - const seller = offerer; - const buyer = recipient; - - // Ensure accounts exist - await upsertAccount(context, seller); - await upsertAccount(context, buyer); - - // Get payment details - const { currencyAddress, totalAmount } = validateAndGetPaymentDetails(paymentItems); - - const contractAddress = nftItem.token; - const tokenId = nftItem.identifier.toString(); - - // Get domain ID - const domainId = getDomainIdByTokenId(config.namespace, contractAddress, tokenId); - - // Record the sale - await context.db.insert(schema.nameSold).values({ - ...sharedEventValues(context.chain.id, event), - fromOwnerId: seller, - newOwnerId: buyer, - currencyAddress: currencyAddress, - chainId: context.chain.id, - logIndex: event.log.logIndex, - orderHash: orderHash, - price: totalAmount, - contractAddress: contractAddress, - tokenId: tokenId, - tokenType: nftItem.itemType === ItemType.ERC721 ? "ERC721" : "ERC1155", - domainId: domainId, - createdAt: event.block.timestamp, - }); + // Get payment details + const paymentDetails = validateAndGetPaymentDetails(paymentItems)!; + + const {orderHash, offerer, recipient} = event.args; + + // In a fulfilled listing, the offerer is selling the NFT, recipient is buying + const seller = offerer; + const buyer = recipient; + + // Ensure accounts exist + await upsertAccount(context, seller); + await upsertAccount(context, buyer); + + const contractAddress = nftItem.token; + const tokenId = nftItem.identifier.toString(); + + // Get domain ID + const tokenIdHex = uint256ToHex32(BigInt(tokenId)); + const domainId = getDomainIdByTokenId(config.namespace, contractAddress, tokenIdHex); + + // Record the sale + await context.db.insert(schema.nameSold).values({ + ...sharedEventValues(context.chain.id, event), + fromOwnerId: seller, + newOwnerId: buyer, + currencyAddress: paymentDetails.currencyAddress, + chainId: context.chain.id, + logIndex: event.log.logIndex, + orderHash: orderHash, + price: paymentDetails.totalAmount, + contractAddress: contractAddress, + tokenId: tokenId, + tokenType: nftItem.itemType === ItemType.ERC721 ? "ERC721" : "ERC1155", + domainId: domainId, + timestamp: event.block.timestamp, + }); } /** @@ -207,70 +269,133 @@ async function handleListingFulfilled( * and if the token contract is a known token contract */ function isSupportedTokenTypeAndContract( - item: OfferItem | ConsiderationItem, - context: Context, + chainId: number, + item: OfferItem | ConsiderationItem, ): boolean { - if (!item || !item.token) return false; - - const chainId = context.chain.id; - const contractAddress = item.token as Address; - const isValidItemType = item.itemType === ItemType.ERC721 || item.itemType === ItemType.ERC1155; - const isSupportedContract = isKnownTokenIssuingContract(config.namespace, { - chainId, - address: contractAddress, - }); - - return isValidItemType && isSupportedContract; + const contractAddress = item.token; + const isValidItemType = item.itemType === ItemType.ERC721 || item.itemType === ItemType.ERC1155; + const isSupportedContract = isKnownTokenIssuingContract(config.namespace, { + chainId, + address: contractAddress, + }); + + return isValidItemType && isSupportedContract; } /** * Finds all payment items from offer array */ function findPaymentItemsInOffer(offer: readonly OfferItem[]): OfferItem[] { - return offer.filter( - (item) => item.itemType === ItemType.NATIVE || item.itemType === ItemType.ERC20, - ); + return offer.filter( + (item) => item.itemType === ItemType.NATIVE || item.itemType === ItemType.ERC20, + ); } /** * Finds all payment items from consideration array */ function findPaymentItemsInConsideration( - consideration: readonly ConsiderationItem[], + consideration: readonly ConsiderationItem[], ): ConsiderationItem[] { - return consideration.filter( - (item) => item.itemType === ItemType.NATIVE || item.itemType === ItemType.ERC20, - ); + return consideration.filter( + (item) => item.itemType === ItemType.NATIVE || item.itemType === ItemType.ERC20, + ); +} + +/** + * Validates if we have a valid listing scenario: + * - Exactly one supported NFT in offer + * - At least one payment item in consideration + * - Valid payment configuration + */ +function validateListing( + chainId: number, + offer: readonly OfferItem[], + consideration: readonly ConsiderationItem[], +): { nftItem: OfferItem; paymentItems: ConsiderationItem[] } | null { + // Find NFTs in offer (should be exactly one for our use case) + const nftsInOffer = offer.filter((item) => isSupportedTokenTypeAndContract(chainId, item)); + if (nftsInOffer.length !== 1) { + return null; // We only support single NFT listings + } + + const paymentItems = findPaymentItemsInConsideration(consideration); + if (paymentItems.length === 0) { + return null; // No payment items + } + + // Pre-validate payment configuration + const paymentDetails = validateAndGetPaymentDetails(paymentItems); + if (!paymentDetails) { + return null; // Invalid payment configuration + } + + return { + nftItem: nftsInOffer[0]!, + paymentItems, + }; +} + +/** + * Validates if we have a valid offer scenario: + * - At least one payment item in offer + * - Exactly one supported NFT in consideration + * - Valid payment configuration + */ +function validateOffer( + chainId: number, + offer: readonly OfferItem[], + consideration: readonly ConsiderationItem[], +): { nftItem: ConsiderationItem; paymentItems: OfferItem[] } | null { + const paymentItems = findPaymentItemsInOffer(offer); + if (paymentItems.length === 0) { + return null; // No payment items + } + + // Find NFTs in consideration (should be exactly one for our use case) + const nftsInConsideration = consideration.filter((item) => + isSupportedTokenTypeAndContract(chainId, item), + ); + if (nftsInConsideration.length !== 1) { + return null; // We only support single NFT offers + } + + // Pre-validate payment configuration + const paymentDetails = validateAndGetPaymentDetails(paymentItems); + if (!paymentDetails) { + return null; // Invalid payment configuration + } + + return { + nftItem: nftsInConsideration[0]!, + paymentItems, + }; } /** * Main handler for Seaport OrderFulfilled events */ export async function handleOrderFulfilled({ - context, - event, -}: { - context: Context; - event: SeaportOrderFulfilledEvent; + context, + event, + }: { + context: Context; + event: SeaportOrderFulfilledEvent; }) { - const { offer, consideration } = event.args; - - // Check if this is a listing (NFT in offer, payment in consideration) - const nftInOffer = offer.find((item) => isSupportedTokenTypeAndContract(item, context)); - const paymentItemsInConsideration = findPaymentItemsInConsideration(consideration); - - if (nftInOffer && paymentItemsInConsideration.length > 0) { - await handleListingFulfilled(context, event, nftInOffer, paymentItemsInConsideration); - return; - } - - // Check if this is an offer (payment in offer, NFT in consideration) - const paymentItemsInOffer = findPaymentItemsInOffer(offer); - const nftInConsideration = consideration.find((item) => - isSupportedTokenTypeAndContract(item, context), - ); - - if (paymentItemsInOffer.length > 0 && nftInConsideration) { - await handleOfferFulfilled(context, event, nftInConsideration, paymentItemsInOffer); - } -} + const {offer, consideration} = event.args; + const chainId = context.chain.id; + + // Try to validate as a listing first + const listingValidation = validateListing(chainId, offer, consideration); + if (listingValidation) { + await handleListingFulfilled(context, event, listingValidation.nftItem, listingValidation.paymentItems); + return; + } + + // Try to validate as an offer + const offerValidation = validateOffer(chainId, offer, consideration); + if (offerValidation) { + await handleOfferFulfilled(context, event, offerValidation.nftItem, offerValidation.paymentItems); + return; + } +} \ No newline at end of file diff --git a/apps/ensindexer/src/plugins/referrals/handlers/UnwrappedEthRegistrarController.ts b/apps/ensindexer/src/plugins/referrals/handlers/UnwrappedEthRegistrarController.ts index 525838996..837eaa786 100644 --- a/apps/ensindexer/src/plugins/referrals/handlers/UnwrappedEthRegistrarController.ts +++ b/apps/ensindexer/src/plugins/referrals/handlers/UnwrappedEthRegistrarController.ts @@ -1,11 +1,8 @@ import { ponder } from "ponder:registry"; -import { namehash } from "viem"; import { handleRegistrationReferral, handleRenewalReferral } from "@/handlers/Referrals"; import { namespaceContract } from "@/lib/plugin-helpers"; -import { PluginName, makeSubdomainNode } from "@ensnode/ensnode-sdk"; - -const ETH_NODE = namehash("eth"); +import { PluginName, makeSubdomainNode, ETH_NODE } from "@ensnode/ensnode-sdk"; /** * Registers event handlers with Ponder. diff --git a/packages/datasources/src/index.ts b/packages/datasources/src/index.ts index dcb808191..2de5c98f5 100644 --- a/packages/datasources/src/index.ts +++ b/packages/datasources/src/index.ts @@ -1,39 +1,39 @@ -import { makeSubdomainNode } from "@ensnode/ensnode-sdk"; -import { Address, Hex, toHex } from "viem"; +import {makeSubdomainNode, ETH_NODE} from "@ensnode/ensnode-sdk"; +import {Address, Hex} from "viem"; import { - base, - baseSepolia, - holesky as holeskyChain, - linea, - lineaSepolia, - mainnet as mainnetChain, - optimism, - sepolia as sepoliaChain, + base, + baseSepolia, + holesky as holeskyChain, + linea, + lineaSepolia, + mainnet as mainnetChain, + optimism, + sepolia as sepoliaChain, } from "viem/chains"; import ensTestEnv from "./ens-test-env"; import holesky from "./holesky"; -import { DatasourceNames, ENSNamespace, ENSNamespaceId, ENSNamespaceIds } from "./lib/types"; +import {DatasourceNames, ENSNamespace, ENSNamespaceId, ENSNamespaceIds} from "./lib/types"; import mainnet from "./mainnet"; import sepolia from "./sepolia"; export * from "./lib/types"; // export the shared ResolverABI for consumer convenience -export { ResolverABI } from "./lib/resolver"; +export {ResolverABI} from "./lib/resolver"; /** * Identifies a specific address on a specific chain. */ export interface ChainAddress { - chainId: number; - address: Address; + chainId: number; + address: Address; } // internal map ENSNamespaceId -> ENSNamespace const ENSNamespacesById = { - mainnet, - sepolia, - holesky, - "ens-test-env": ensTestEnv, + mainnet, + sepolia, + holesky, + "ens-test-env": ensTestEnv, } as const satisfies Record; /** @@ -43,7 +43,7 @@ const ENSNamespacesById = { * @returns the ENSNamespace */ export const getENSNamespace = ( - namespaceId: N, + namespaceId: N, ): (typeof ENSNamespacesById)[N] => ENSNamespacesById[namespaceId]; /** @@ -57,11 +57,11 @@ export const getENSNamespace = ( * @returns The Datasource object for the given name within the specified namespace */ export const getDatasource = < - N extends ENSNamespaceId, - D extends keyof ReturnType>, + N extends ENSNamespaceId, + D extends keyof ReturnType>, >( - namespaceId: N, - datasourceName: D, + namespaceId: N, + datasourceName: D, ) => getENSNamespace(namespaceId)[datasourceName]; /** @@ -70,7 +70,7 @@ export const getDatasource = < * @returns the chain that hosts the ENS Root */ export const getENSRootChain = (namespaceId: ENSNamespaceId) => - getDatasource(namespaceId, DatasourceNames.ENSRoot).chain; + getDatasource(namespaceId, DatasourceNames.ENSRoot).chain; /** * Returns the chain id for the ENS Root Datasource within the selected namespace. @@ -85,7 +85,7 @@ export const getENSRootChainId = (namespaceId: ENSNamespaceId) => getENSRootChai * @returns the viem#Address object */ export const getNameWrapperAddress = (namespaceId: ENSNamespaceId): Address => - getDatasource(namespaceId, DatasourceNames.ENSRoot).contracts.NameWrapper.address; + getDatasource(namespaceId, DatasourceNames.ENSRoot).contracts.NameWrapper.address; /** * Get the ENS Manager App URL for the provided namespace. @@ -94,17 +94,17 @@ export const getNameWrapperAddress = (namespaceId: ENSNamespaceId): Address => * @returns ENS Manager App URL for the provided namespace, or null if the provided namespace doesn't have a known ENS Manager App */ export function getEnsManagerAppUrl(namespaceId: ENSNamespaceId): URL | null { - switch (namespaceId) { - case ENSNamespaceIds.Mainnet: - return new URL(`https://app.ens.domains/`); - case ENSNamespaceIds.Sepolia: - return new URL(`https://sepolia.app.ens.domains/`); - case ENSNamespaceIds.Holesky: - return new URL(`https://holesky.app.ens.domains/`); - case ENSNamespaceIds.EnsTestEnv: - // ens-test-env runs on a local chain and is not supported by app.ens.domains - return null; - } + switch (namespaceId) { + case ENSNamespaceIds.Mainnet: + return new URL(`https://app.ens.domains/`); + case ENSNamespaceIds.Sepolia: + return new URL(`https://sepolia.app.ens.domains/`); + case ENSNamespaceIds.Holesky: + return new URL(`https://holesky.app.ens.domains/`); + case ENSNamespaceIds.EnsTestEnv: + // ens-test-env runs on a local chain and is not supported by app.ens.domains + return null; + } } /** @@ -115,18 +115,18 @@ export function getEnsManagerAppUrl(namespaceId: ENSNamespaceId): URL | null { * @returns avatar image URL for the name on the given ENS Namespace, or null if the avatar image URL is not known */ export function getNameAvatarUrl(name: string, namespaceId: ENSNamespaceId): URL | null { - switch (namespaceId) { - case ENSNamespaceIds.Mainnet: - return new URL(name, `https://metadata.ens.domains/mainnet/avatar/`); - case ENSNamespaceIds.Sepolia: - return new URL(name, `https://metadata.ens.domains/sepolia/avatar/`); - case ENSNamespaceIds.Holesky: - // metadata.ens.domains doesn't currently support holesky - return null; - case ENSNamespaceIds.EnsTestEnv: - // ens-test-env runs on a local chain and is not supported by metadata.ens.domains - return null; - } + switch (namespaceId) { + case ENSNamespaceIds.Mainnet: + return new URL(name, `https://metadata.ens.domains/mainnet/avatar/`); + case ENSNamespaceIds.Sepolia: + return new URL(name, `https://metadata.ens.domains/sepolia/avatar/`); + case ENSNamespaceIds.Holesky: + // metadata.ens.domains doesn't currently support holesky + return null; + case ENSNamespaceIds.EnsTestEnv: + // ens-test-env runs on a local chain and is not supported by metadata.ens.domains + return null; + } } /** @@ -135,9 +135,9 @@ export function getNameAvatarUrl(name: string, namespaceId: ENSNamespaceId): URL * @returns URL to the name details page in the ENS Manager App for a given name and ENS Namespace, or null if this URL is not known */ export function getNameDetailsUrl(name: string, namespaceId: ENSNamespaceId): URL | null { - const baseUrl = getEnsManagerAppUrl(namespaceId); + const baseUrl = getEnsManagerAppUrl(namespaceId); - return baseUrl ? new URL(name, baseUrl) : null; + return baseUrl ? new URL(name, baseUrl) : null; } /** @@ -146,9 +146,9 @@ export function getNameDetailsUrl(name: string, namespaceId: ENSNamespaceId): UR * @returns URL to the address details page in the ENS Manager App for a given address and ENS Namespace, or null if this URL is not known */ export function getAddressDetailsUrl(address: Address, namespaceId: ENSNamespaceId): URL | null { - const baseUrl = getEnsManagerAppUrl(namespaceId); + const baseUrl = getEnsManagerAppUrl(namespaceId); - return baseUrl ? new URL(address, baseUrl) : null; + return baseUrl ? new URL(address, baseUrl) : null; } /** @@ -156,14 +156,14 @@ export function getAddressDetailsUrl(address: Address, namespaceId: ENSNamespace * Chain id standards are organized by the Ethereum Community @ https://github.com/ethereum-lists/chains */ const chainBlockExplorers = new Map([ - [mainnetChain.id, "https://etherscan.io"], - [base.id, "https://basescan.org"], - [sepoliaChain.id, "https://sepolia.etherscan.io"], - [optimism.id, "https://optimistic.etherscan.io"], - [linea.id, "https://lineascan.build"], - [holeskyChain.id, "https://holesky.etherscan.io"], - [baseSepolia.id, "https://sepolia.basescan.org"], - [lineaSepolia.id, "https://sepolia.lineascan.build"], + [mainnetChain.id, "https://etherscan.io"], + [base.id, "https://basescan.org"], + [sepoliaChain.id, "https://sepolia.etherscan.io"], + [optimism.id, "https://optimistic.etherscan.io"], + [linea.id, "https://lineascan.build"], + [holeskyChain.id, "https://holesky.etherscan.io"], + [baseSepolia.id, "https://sepolia.basescan.org"], + [lineaSepolia.id, "https://sepolia.lineascan.build"], ]); /** @@ -173,13 +173,13 @@ const chainBlockExplorers = new Map([ * or null if the referenced chain doesn't have a known block explorer */ export const getChainBlockExplorerUrl = (chainId: number): URL | null => { - const chainBlockExplorer = chainBlockExplorers.get(chainId); + const chainBlockExplorer = chainBlockExplorers.get(chainId); - if (!chainBlockExplorer) { - return null; - } + if (!chainBlockExplorer) { + return null; + } - return new URL(chainBlockExplorer); + return new URL(chainBlockExplorer); }; /** @@ -189,12 +189,12 @@ export const getChainBlockExplorerUrl = (chainId: number): URL | null => { * or null if the referenced chain doesn't have a known block explorer */ export const getBlockExplorerUrlForBlock = (chainId: number, blockNumber: number): URL | null => { - const chainBlockExplorer = getChainBlockExplorerUrl(chainId); + const chainBlockExplorer = getChainBlockExplorerUrl(chainId); - if (!chainBlockExplorer) { - return null; - } - return new URL(`block/${blockNumber}`, chainBlockExplorer.toString()); + if (!chainBlockExplorer) { + return null; + } + return new URL(`block/${blockNumber}`, chainBlockExplorer.toString()); }; /** @@ -202,15 +202,15 @@ export const getBlockExplorerUrlForBlock = (chainId: number, blockNumber: number * Chain id standards are organized by the Ethereum Community @ https://github.com/ethereum-lists/chains */ const chainNames = new Map([ - [mainnetChain.id, "Ethereum"], - [base.id, "Base"], - [sepoliaChain.id, "Ethereum Sepolia"], - [optimism.id, "Optimism"], - [linea.id, "Linea"], - [holeskyChain.id, "Ethereum Holesky"], - [1337, "Ethereum Local"], // ens-test-env runs on a local Anvil chain with id 1337 - [baseSepolia.id, "Base Sepolia"], - [lineaSepolia.id, "Linea Sepolia"], + [mainnetChain.id, "Ethereum"], + [base.id, "Base"], + [sepoliaChain.id, "Ethereum Sepolia"], + [optimism.id, "Optimism"], + [linea.id, "Linea"], + [holeskyChain.id, "Ethereum Holesky"], + [1337, "Ethereum Local"], // ens-test-env runs on a local Anvil chain with id 1337 + [baseSepolia.id, "Base Sepolia"], + [lineaSepolia.id, "Linea Sepolia"], ]); /** @@ -218,13 +218,13 @@ const chainNames = new Map([ * or throws an error if the provided chain id doesn't have an assigned name. */ export function getChainName(chainId: number): string { - const chainName = chainNames.get(chainId); + const chainName = chainNames.get(chainId); - if (!chainName) { - throw new Error(`Chain ID "${chainId}" doesn't have an assigned name`); - } + if (!chainName) { + throw new Error(`Chain ID "${chainId}" doesn't have an assigned name`); + } - return chainName; + return chainName; } /** @@ -234,95 +234,95 @@ export function getChainName(chainId: number): string { * @returns an array of 0 or more ChainAddress objects */ export const getKnownTokenIssuingContracts = (namespaceId: ENSNamespaceId): ChainAddress[] => { - switch (namespaceId) { - case ENSNamespaceIds.Mainnet: { - const rootDatasource = getDatasource(namespaceId, DatasourceNames.ENSRoot); - const lineanamesDatasource = getDatasource(namespaceId, DatasourceNames.Lineanames); - const basenamesDatasource = getDatasource(namespaceId, DatasourceNames.Basenames); - const threeDnsBaseDatasource = getDatasource(namespaceId, DatasourceNames.ThreeDNSBase); - const threeDnsOptimismDatasource = getDatasource( - namespaceId, - DatasourceNames.ThreeDNSOptimism, - ); - return [ - { - // ENS Token - Mainnet - chainId: rootDatasource.chain.id, - address: rootDatasource.contracts["BaseRegistrar"].address, - }, - // NameWrapper Token - Mainnet - { - chainId: rootDatasource.chain.id, - address: rootDatasource.contracts["NameWrapper"].address, - }, - // 3DNS Token - Optimism - { - chainId: threeDnsOptimismDatasource.chain.id, - address: threeDnsOptimismDatasource.contracts["ThreeDNSToken"].address, - }, - // 3DNS Token - Base - { - chainId: threeDnsBaseDatasource.chain.id, - address: threeDnsBaseDatasource.contracts["ThreeDNSToken"].address, - }, - // Linear Names Token - Base - { - chainId: lineanamesDatasource.chain.id, - address: lineanamesDatasource.contracts["BaseRegistrar"].address, - }, - // Base Names Token - Base - { - chainId: basenamesDatasource.chain.id, - address: basenamesDatasource.contracts["BaseRegistrar"].address, - }, - ]; - } - case ENSNamespaceIds.Sepolia: { - const rootDatasource = getDatasource(namespaceId, DatasourceNames.ENSRoot); - return [ - { - // ENS Token - Sepolia - chainId: rootDatasource.chain.id, - address: rootDatasource.contracts["BaseRegistrar"].address, - }, - { - // NameWrapper Token - Sepolia - chainId: rootDatasource.chain.id, - address: rootDatasource.contracts["NameWrapper"].address, - }, - ]; - } - case ENSNamespaceIds.Holesky: { - const rootDatasource = getDatasource(namespaceId, DatasourceNames.ENSRoot); - return [ - { - // ENS Token - Holesky - chainId: rootDatasource.chain.id, - address: rootDatasource.contracts["BaseRegistrar"].address, - }, - { - // NameWrapper Token - Holesky - chainId: rootDatasource.chain.id, - address: rootDatasource.contracts["NameWrapper"].address, - }, - ]; - } - case ENSNamespaceIds.EnsTestEnv: { - const rootDatasource = getDatasource(namespaceId, DatasourceNames.ENSRoot); - return [ - { - // ENS Token - EnsTestEnv - chainId: rootDatasource.chain.id, - address: rootDatasource.contracts["BaseRegistrar"].address, - }, - { - // NameWrapper Token - EnsTestEnv - chainId: rootDatasource.chain.id, - address: rootDatasource.contracts["NameWrapper"].address, - }, - ]; + switch (namespaceId) { + case ENSNamespaceIds.Mainnet: { + const rootDatasource = getDatasource(namespaceId, DatasourceNames.ENSRoot); + const lineanamesDatasource = getDatasource(namespaceId, DatasourceNames.Lineanames); + const basenamesDatasource = getDatasource(namespaceId, DatasourceNames.Basenames); + const threeDnsBaseDatasource = getDatasource(namespaceId, DatasourceNames.ThreeDNSBase); + const threeDnsOptimismDatasource = getDatasource( + namespaceId, + DatasourceNames.ThreeDNSOptimism, + ); + return [ + // Eth Token - Mainnet + { + chainId: rootDatasource.chain.id, + address: rootDatasource.contracts["BaseRegistrar"].address, + }, + // NameWrapper Token - Mainnet + { + chainId: rootDatasource.chain.id, + address: rootDatasource.contracts["NameWrapper"].address, + }, + // 3DNS Token - Optimism + { + chainId: threeDnsOptimismDatasource.chain.id, + address: threeDnsOptimismDatasource.contracts["ThreeDNSToken"].address, + }, + // 3DNS Token - Base + { + chainId: threeDnsBaseDatasource.chain.id, + address: threeDnsBaseDatasource.contracts["ThreeDNSToken"].address, + }, + // Linea Names Token - Linea + { + chainId: lineanamesDatasource.chain.id, + address: lineanamesDatasource.contracts["BaseRegistrar"].address, + }, + // Base Names Token - Base + { + chainId: basenamesDatasource.chain.id, + address: basenamesDatasource.contracts["BaseRegistrar"].address, + }, + ]; + } + case ENSNamespaceIds.Sepolia: { + const rootDatasource = getDatasource(namespaceId, DatasourceNames.ENSRoot); + return [ + { + // ENS Token - Sepolia + chainId: rootDatasource.chain.id, + address: rootDatasource.contracts["BaseRegistrar"].address, + }, + { + // NameWrapper Token - Sepolia + chainId: rootDatasource.chain.id, + address: rootDatasource.contracts["NameWrapper"].address, + }, + ]; + } + case ENSNamespaceIds.Holesky: { + const rootDatasource = getDatasource(namespaceId, DatasourceNames.ENSRoot); + return [ + { + // ENS Token - Holesky + chainId: rootDatasource.chain.id, + address: rootDatasource.contracts["BaseRegistrar"].address, + }, + { + // NameWrapper Token - Holesky + chainId: rootDatasource.chain.id, + address: rootDatasource.contracts["NameWrapper"].address, + }, + ]; + } + case ENSNamespaceIds.EnsTestEnv: { + const rootDatasource = getDatasource(namespaceId, DatasourceNames.ENSRoot); + return [ + { + // ENS Token - EnsTestEnv + chainId: rootDatasource.chain.id, + address: rootDatasource.contracts["BaseRegistrar"].address, + }, + { + // NameWrapper Token - EnsTestEnv + chainId: rootDatasource.chain.id, + address: rootDatasource.contracts["NameWrapper"].address, + }, + ]; + } } - } }; /** @@ -333,38 +333,46 @@ export const getKnownTokenIssuingContracts = (namespaceId: ENSNamespaceId): Chai * @returns a boolean indicating whether the provided ChainAddress is a known token issuing contract */ export const isKnownTokenIssuingContract = ( - namespaceId: ENSNamespaceId, - chainAddress: ChainAddress, + namespaceId: ENSNamespaceId, + chainAddress: ChainAddress, ): boolean => { - const knownContracts = getKnownTokenIssuingContracts(namespaceId); - return knownContracts.some( - (knownContract) => - knownContract.chainId === chainAddress.chainId && - knownContract.address.toLowerCase() === chainAddress.address.toLowerCase(), - ); + const knownContracts = getKnownTokenIssuingContracts(namespaceId); + return knownContracts.some((contract) => isEqualChainAddress(contract, chainAddress)); +}; + +/** + * Returns a boolean indicating whether the provided ChainAddress objects are equal. + * + * @param address1 - The first ChainAddress to compare + * @param address2 - The second ChainAddress to compare + * @returns a boolean indicating whether the provided ChainAddress objects are equal + */ +export const isEqualChainAddress = (address1: ChainAddress, address2: ChainAddress): boolean => { + return ( + address1.chainId === address2.chainId && + address1.address.toLowerCase() === address2.address.toLowerCase() + ); }; /** * Get the domainId by contract address and tokenId * @param namespaceId - The ENSNamespace identifier (e.g. 'mainnet', 'sepolia', 'holesky', 'ens-test-env') * @param contractAddress - contract address of the NFT - * @param tokenId - tokenId of the NFT + * @param tokenIdHex - tokenId of the NFT in hex */ export function getDomainIdByTokenId( - namespaceId: ENSNamespaceId, - contractAddress: Address, - tokenId: string, + namespaceId: ENSNamespaceId, + contractAddress: Address, + tokenIdHex: Hex, ): Hex { - const tokenIdHex = `0x${BigInt(tokenId).toString(16).padStart(64, "0")}` as Hex; - const baseRegistrarContractAddress = getDatasource(namespaceId, DatasourceNames.ENSRoot) - .contracts["BaseRegistrar"].address; + const baseRegistrarContractAddress = getDatasource(namespaceId, DatasourceNames.ENSRoot) + .contracts["BaseRegistrar"].address; - // OLD ENS Registry: tokenId is labelhash so need to convert to namehash - if (contractAddress === baseRegistrarContractAddress) { - const ETH_PARENT_NODE = "0x93cdeb708b7545dc668eb9280176169d1c33cfd8ed6f04690a0bcc88a93fc4ae"; - return makeSubdomainNode(tokenIdHex, ETH_PARENT_NODE); - } + // OLD ENS Registry: tokenId is labelhash so need to convert to namehash + if (contractAddress === baseRegistrarContractAddress) { + return makeSubdomainNode(tokenIdHex, ETH_NODE); + } - // for other names we for now assume it is already namehash - return tokenIdHex; -} + // for other names we for now assume it is already namehash + return tokenIdHex; +} \ No newline at end of file diff --git a/packages/ensnode-schema/src/tokenscope.schema.ts b/packages/ensnode-schema/src/tokenscope.schema.ts index b44d593e2..b4a088f8f 100644 --- a/packages/ensnode-schema/src/tokenscope.schema.ts +++ b/packages/ensnode-schema/src/tokenscope.schema.ts @@ -1,85 +1,86 @@ -import { index, onchainTable } from "ponder"; +import {index, onchainTable} from "ponder"; const sharedEventColumns = (t: any) => ({ - /** - * The unique identifier of the event - * This is a composite key made up of: - * - chainId - * - blockNumber - * - logIndex - * e.g. 1-1234-5 - */ - id: t.text().primaryKey(), - /** - * The block number of the event - */ - blockNumber: t.integer().notNull(), - /** - * The log index of the event - */ - logIndex: t.integer().notNull(), - /** - * The transaction hash of the event - */ - transactionID: t.hex().notNull(), - /** - * The chain ID of the event - */ - chainId: t.integer().notNull(), -}); - -export const nameSold = onchainTable( - "name_sold", - (t) => ({ - ...sharedEventColumns(t), - /** - * The account that sold the name - */ - fromOwnerId: t.hex().notNull(), - /** - * The account that received the name - */ - newOwnerId: t.hex().notNull(), - /** - * Currency address of the payment - */ - currencyAddress: t.hex().notNull(), - /** - * The amount of the payment - */ - price: t.bigint().notNull(), - /** - * The unique hash identifier of the fulfilled order. - * Used to track and reference specific orders on-chain. - */ - orderHash: t.hex().notNull(), /** - * The ID of the token being sold + * The unique identifier of the event + * This is a composite key made up of: + * - chainId + * - blockNumber + * - logIndex + * e.g. 1-1234-5 */ - tokenId: t.text().notNull(), + id: t.text().primaryKey(), /** - * The contract address of the token being sold + * The block number of the event */ - contractAddress: t.hex().notNull(), + blockNumber: t.integer().notNull(), /** - * The namehash of the name + * The log index of the event */ - domainId: t.hex().notNull(), + logIndex: t.integer().notNull(), /** - * The time when the order was created + * The transaction hash of the event */ - createdAt: t.bigint().notNull(), + transactionID: t.hex().notNull(), /** - * The type of token being sold - * can either be ERC721 or ERC1155 + * The chain ID of the event */ - tokenType: t.text().notNull(), - }), - (t) => ({ - idx_from: index().on(t.fromOwnerId), - idx_to: index().on(t.newOwnerId), - idx_domain: index().on(t.domainId), - idx_compound: index().on(t.fromOwnerId, t.id), - idx_created: index().on(t.createdAt), - }), + chainId: t.integer().notNull(), +}); + +export const nameSold = onchainTable( + "name_sold", + (t) => ({ + ...sharedEventColumns(t), + /** + * The account that sold the name + */ + fromOwnerId: t.hex().notNull(), + /** + * The account that received the name + */ + newOwnerId: t.hex().notNull(), + /** + * Currency address of the payment + * Can either be ETH, USDC, WETH or DAI + */ + currency: t.text().notNull(), + /** + * The amount of the payment + */ + price: t.bigint().notNull(), + /** + * Order hashes are generated by the Seaport protocol which basically takes all the order components (considerations, offers etc) + * to create a unique hash, which is used to track and reference the order. + */ + orderHash: t.hex().notNull(), + /** + * The ID of the token being sold + */ + tokenId: t.text().notNull(), + /** + * The contract address of the token being sold + */ + contractAddress: t.hex().notNull(), + /** + * The namehash of the name + */ + domainId: t.hex().notNull(), + /** + * Unix time stamp of when the name was sold (block timestamp) + */ + timestamp: t.bigint().notNull(), + /** + * The type of token being sold + * can either be ERC721 or ERC1155 + */ + tokenType: t.text().notNull(), + }), + (t) => ({ + idx_from: index().on(t.fromOwnerId), + idx_to: index().on(t.newOwnerId), + idx_domain: index().on(t.domainId), + idx_compound: index().on(t.fromOwnerId, t.id), + idx_created: index().on(t.timestamp), + }), ); diff --git a/packages/ensnode-sdk/src/ens/constants.ts b/packages/ensnode-sdk/src/ens/constants.ts new file mode 100644 index 000000000..d9f06e320 --- /dev/null +++ b/packages/ensnode-sdk/src/ens/constants.ts @@ -0,0 +1,3 @@ +import {namehash} from "viem"; + +export const ETH_NODE = namehash("eth"); \ No newline at end of file diff --git a/packages/ensnode-sdk/src/ens/index.ts b/packages/ensnode-sdk/src/ens/index.ts new file mode 100644 index 000000000..d23448b43 --- /dev/null +++ b/packages/ensnode-sdk/src/ens/index.ts @@ -0,0 +1 @@ +export * from "./constants"; \ No newline at end of file diff --git a/packages/ensnode-sdk/src/index.ts b/packages/ensnode-sdk/src/index.ts index 9e7d3215a..fc67f2aad 100644 --- a/packages/ensnode-sdk/src/index.ts +++ b/packages/ensnode-sdk/src/index.ts @@ -1,2 +1,3 @@ export * from "./utils"; +export * from "./ens"; export * from "./tracing"; From a78c9271198d3beb42ef8e381c2e765e81fe99b8 Mon Sep 17 00:00:00 2001 From: Zimtente Date: Mon, 25 Aug 2025 08:04:49 +0200 Subject: [PATCH 04/61] last changes --- apps/ensadmin/src/lib/namespace-utils.ts | 364 +--------------- apps/ensindexer/src/handlers/Seaport.ts | 18 +- apps/ensindexer/src/lib/tokenscope-helpers.ts | 397 ++++++++++++++++++ packages/ensnode-sdk/src/ens/constants.ts | 1 + 4 files changed, 413 insertions(+), 367 deletions(-) create mode 100644 apps/ensindexer/src/lib/tokenscope-helpers.ts diff --git a/apps/ensadmin/src/lib/namespace-utils.ts b/apps/ensadmin/src/lib/namespace-utils.ts index e2d45a02a..cefe1732a 100644 --- a/apps/ensadmin/src/lib/namespace-utils.ts +++ b/apps/ensadmin/src/lib/namespace-utils.ts @@ -4,8 +4,8 @@ import { ENSNamespaceIds, getDatasource, } from "@ensnode/datasources"; -import { ChainId, ETH_NODE, Name, makeSubdomainNode } from "@ensnode/ensnode-sdk"; -import { Address, Hex } from "viem"; +import { ChainId, Name } from "@ensnode/ensnode-sdk"; +import { Address } from "viem"; import { anvil, arbitrum, @@ -25,28 +25,6 @@ import { const ensTestEnv = { ...anvil, id: 1337, name: "ens-test-env" }; -/** - * Identifies a specific address on a specific chain. - */ -export interface ChainAddress { - chainId: ChainId; - address: Address; -} - -export interface Currency { - symbol: string; - name: string; - decimals: number; - // For native currencies, address will be null - address: Address | null; -} - -export interface ChainCurrency extends Currency { - chainId: ChainId; -} - -const NATIVE_CURRENCY_SYMBOL = "NATIVE" as const; - const SUPPORTED_CHAINS = [ ensTestEnv, mainnet, @@ -200,341 +178,3 @@ export function getChainName(chainId: ChainId): string { const name = CUSTOM_CHAIN_NAMES.get(chainId); return name || `Unknown Chain (${chainId})`; } - -/** - * Returns an array of 0 or more ChainAddress objects that are known to provide tokenized name ownership. - * - * @param namespaceId - The ENSNamespace identifier (e.g. 'mainnet', 'sepolia', 'holesky', 'ens-test-env') - * @returns an array of 0 or more ChainAddress objects - */ -export const getKnownTokenIssuingContracts = (namespaceId: ENSNamespaceId): ChainAddress[] => { - switch (namespaceId) { - case ENSNamespaceIds.Mainnet: { - const rootDatasource = getDatasource(namespaceId, DatasourceNames.ENSRoot); - const lineanamesDatasource = getDatasource(namespaceId, DatasourceNames.Lineanames); - const basenamesDatasource = getDatasource(namespaceId, DatasourceNames.Basenames); - const threeDnsBaseDatasource = getDatasource(namespaceId, DatasourceNames.ThreeDNSBase); - const threeDnsOptimismDatasource = getDatasource( - namespaceId, - DatasourceNames.ThreeDNSOptimism, - ); - return [ - // Eth Token - Mainnet - { - chainId: rootDatasource.chain.id, - address: rootDatasource.contracts["BaseRegistrar"].address, - }, - // NameWrapper Token - Mainnet - { - chainId: rootDatasource.chain.id, - address: rootDatasource.contracts["NameWrapper"].address, - }, - // 3DNS Token - Optimism - { - chainId: threeDnsOptimismDatasource.chain.id, - address: threeDnsOptimismDatasource.contracts["ThreeDNSToken"].address, - }, - // 3DNS Token - Base - { - chainId: threeDnsBaseDatasource.chain.id, - address: threeDnsBaseDatasource.contracts["ThreeDNSToken"].address, - }, - // Linea Names Token - Linea - { - chainId: lineanamesDatasource.chain.id, - address: lineanamesDatasource.contracts["BaseRegistrar"].address, - }, - // Base Names Token - Base - { - chainId: basenamesDatasource.chain.id, - address: basenamesDatasource.contracts["BaseRegistrar"].address, - }, - ]; - } - case ENSNamespaceIds.Sepolia: { - const rootDatasource = getDatasource(namespaceId, DatasourceNames.ENSRoot); - const basenamesDatasource = getDatasource(namespaceId, DatasourceNames.Basenames); - const lineanamesDatasource = getDatasource(namespaceId, DatasourceNames.Lineanames); - - return [ - { - // ENS Token - Sepolia - chainId: rootDatasource.chain.id, - address: rootDatasource.contracts["BaseRegistrar"].address, - }, - { - // NameWrapper Token - Sepolia - chainId: rootDatasource.chain.id, - address: rootDatasource.contracts["NameWrapper"].address, - }, - { - // Basenames Token - Base Sepolia - chainId: basenamesDatasource.chain.id, - address: basenamesDatasource.contracts["BaseRegistrar"].address, - }, - { - // Lineanames Token - Linea Sepolia - chainId: lineanamesDatasource.chain.id, - address: lineanamesDatasource.contracts["BaseRegistrar"].address, - }, - ]; - } - case ENSNamespaceIds.Holesky: { - const rootDatasource = getDatasource(namespaceId, DatasourceNames.ENSRoot); - return [ - { - // ENS Token - Holesky - chainId: rootDatasource.chain.id, - address: rootDatasource.contracts["BaseRegistrar"].address, - }, - { - // NameWrapper Token - Holesky - chainId: rootDatasource.chain.id, - address: rootDatasource.contracts["NameWrapper"].address, - }, - ]; - } - case ENSNamespaceIds.EnsTestEnv: { - const rootDatasource = getDatasource(namespaceId, DatasourceNames.ENSRoot); - return [ - { - // ENS Token - EnsTestEnv - chainId: rootDatasource.chain.id, - address: rootDatasource.contracts["BaseRegistrar"].address, - }, - { - // NameWrapper Token - EnsTestEnv - chainId: rootDatasource.chain.id, - address: rootDatasource.contracts["NameWrapper"].address, - }, - ]; - } - } -}; - -/** - * Returns a boolean indicating whether the provided ChainAddress is a known token issuing contract. - * - * @param namespaceId - The ENSNamespace identifier (e.g. 'mainnet', 'sepolia', 'holesky', 'ens-test-env') - * @param chainAddress - The ChainAddress to check - * @returns a boolean indicating whether the provided ChainAddress is a known token issuing contract - */ -export const isKnownTokenIssuingContract = ( - namespaceId: ENSNamespaceId, - chainAddress: ChainAddress, -): boolean => { - const knownContracts = getKnownTokenIssuingContracts(namespaceId); - return knownContracts.some((contract) => isEqualChainAddress(contract, chainAddress)); -}; - -/** - * Returns a boolean indicating whether the provided ChainAddress objects are equal. - * - * @param address1 - The first ChainAddress to compare - * @param address2 - The second ChainAddress to compare - * @returns a boolean indicating whether the provided ChainAddress objects are equal - */ -export const isEqualChainAddress = (address1: ChainAddress, address2: ChainAddress): boolean => { - return ( - address1.chainId === address2.chainId && - address1.address.toLowerCase() === address2.address.toLowerCase() - ); -}; - -/** - * Get the domainId by contract address and tokenId - * @param chainId - The chainId of the NFT - * @param namespaceId - The ENSNamespace identifier (e.g. 'mainnet', 'sepolia', 'holesky', 'ens-test-env') - * @param contractAddress - contract address of the NFT - * @param tokenIdHex - tokenId of the NFT in hex - */ -export function getDomainIdByTokenId( - chainId: ChainId, - namespaceId: ENSNamespaceId, - contractAddress: Address, - tokenIdHex: Hex, -): Hex { - const baseRegistrarContractAddress = getDatasource(namespaceId, DatasourceNames.ENSRoot) - .contracts["BaseRegistrar"].address; - - // OLD ENS Registry: tokenId is labelhash so need to convert to namehash - if (contractAddress === baseRegistrarContractAddress) { - return makeSubdomainNode(tokenIdHex, ETH_NODE); - } - - // for other names we for now assume it is already namehash - return tokenIdHex; -} - -// Well-known currencies -const ETH_CURRENCY = { - symbol: "ETH", - name: "Ethereum", - decimals: 18, - address: null, -} as const; - -const CHAIN_CURRENCIES = { - // Mainnet - [mainnet.id]: [ - { - symbol: "USDC", - name: "USD Coin", - decimals: 6, - address: "0xA0b86a33E6417c5Dd4Baf8C54e5de49E293E9169" as Address, - }, - { - symbol: "DAI", - name: "Dai Stablecoin", - decimals: 18, - address: "0x6B175474E89094C44Da98b954EedeAC495271d0F" as Address, - }, - ], - // Base - [base.id]: [ - { - symbol: "USDC", - name: "USD Coin", - decimals: 6, - address: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913" as Address, - }, - { - symbol: "DAI", - name: "Dai Stablecoin", - decimals: 18, - address: "0x50c5725949A6F0c72E6C4a641F24049A917DB0Cb" as Address, - }, - ], - // Optimism - [optimism.id]: [ - { - symbol: "USDC", - name: "USD Coin", - decimals: 6, - address: "0x0b2C639c533813f4Aa9D7837CAf62653d097Ff85" as Address, - }, - { - symbol: "DAI", - name: "Dai Stablecoin", - decimals: 18, - address: "0xDA10009cBd5D07dd0CeCc66161FC93D7c9000da1" as Address, - }, - ], - // Linea - [linea.id]: [ - { - symbol: "USDC", - name: "USD Coin", - decimals: 6, - address: "0x176211869cA2b568f2A7D4EE941E073a821EE1ff" as Address, - }, - { - symbol: "DAI", - name: "Dai Stablecoin", - decimals: 18, - address: "0x4AF15ec2A0BD43Db75dd04E62FAA3B8EF36b00d5" as Address, - }, - ], - // Sepolia - [sepolia.id]: [ - { - symbol: "USDC", - name: "USD Coin", - decimals: 6, - address: "0x1c7D4B196Cb0C7B01d743Fbc6116a902379C7238" as Address, - }, - { - symbol: "DAI", - name: "Dai Stablecoin", - decimals: 18, - address: "0x3e622317f8C93f7328350cF0B56d9eD4C620C5d6" as Address, - }, - ], - // Holesky - [holesky.id]: [ - { - symbol: "USDC", - name: "USD Coin", - decimals: 6, - address: "0x1c7D4B196Cb0C7B01d743Fbc6116a902379C7238" as Address, - }, - { - symbol: "DAI", - name: "Dai Stablecoin", - decimals: 18, - address: "0x3e622317f8C93f7328350cF0B56d9eD4C620C5d6" as Address, - }, - ], - // Base Sepolia - [baseSepolia.id]: [ - { - symbol: "USDC", - name: "USD Coin", - decimals: 6, - address: "0x036CbD53842c5426634e7929541eC2318f3dCF7e" as Address, - }, - { - symbol: "DAI", - name: "Dai Stablecoin", - decimals: 18, - address: "0x7368C6C68a4b2b68F90DB2e8F5E3b8E1E5e4F5c7" as Address, - }, - ], - // Linea Sepolia - [lineaSepolia.id]: [ - { - symbol: "USDC", - name: "USD Coin", - decimals: 6, - address: "0x176211869cA2b568f2A7D4EE941E073a821EE1ff" as Address, - }, - { - symbol: "DAI", - name: "Dai Stablecoin", - decimals: 18, - address: "0x4AF15ec2A0BD43Db75dd04E62FAA3B8EF36b00d5" as Address, - }, - ], -} as const; - -/** - * Returns an array of supported currencies for a given chain ID. - * - * @param chainId - The chain ID to get supported currencies for - * @returns an array of ChainCurrency objects representing supported currencies on the chain - */ -export const getSupportedCurrencies = (chainId: ChainId): ChainCurrency[] => { - const chainCurrencies = CHAIN_CURRENCIES[chainId as keyof typeof CHAIN_CURRENCIES] || []; - - // Always add ETH as the native currency - const currencies: ChainCurrency[] = [ - { - ...ETH_CURRENCY, - chainId, - }, - ]; - - // Add chain-specific currencies - currencies.push( - ...chainCurrencies.map((currency) => ({ - ...currency, - chainId, - })), - ); - - return currencies; -}; - -/** - * Returns a boolean indicating whether the provided address is a known supported currency contract. - * - * @param chainId - The chain ID - * @param address - The contract address to check - * @returns a boolean indicating whether the address is a known supported currency contract - */ -export const isKnownCurrencyContract = (chainId: ChainId, address: Address): boolean => { - const supportedCurrencies = getSupportedCurrencies(chainId); - return supportedCurrencies.some( - (currency) => currency.address && currency.address.toLowerCase() === address.toLowerCase(), - ); -}; diff --git a/apps/ensindexer/src/handlers/Seaport.ts b/apps/ensindexer/src/handlers/Seaport.ts index 36fd3695c..f7a4a2d28 100644 --- a/apps/ensindexer/src/handlers/Seaport.ts +++ b/apps/ensindexer/src/handlers/Seaport.ts @@ -9,7 +9,7 @@ import { getDomainIdByTokenId, getSupportedCurrencies, isKnownTokenIssuingContract, -} from "@ensnode/datasources"; +} from "@/lib/tokenscope-helpers"; import { NameSoldInsert, TokenTypes } from "@ensnode/ensnode-schema"; import { ChainId, uint256ToHex32 } from "@ensnode/ensnode-sdk"; import { Address, Hex, zeroAddress } from "viem"; @@ -197,7 +197,7 @@ function getSaleIndexable( paymentsInConsideration.length > 0 ) { // Listing: NFT in offer, payment in consideration - nftItem = nftsInOffer[0]; + nftItem = nftsInOffer[0]!; paymentItems = paymentsInConsideration; seller = offerer; buyer = recipient; @@ -207,7 +207,7 @@ function getSaleIndexable( paymentsInOffer.length > 0 ) { // Offer: payment in offer, NFT in consideration - nftItem = nftsInConsideration[0]; + nftItem = nftsInConsideration[0]!; paymentItems = paymentsInOffer; seller = recipient; buyer = offerer; @@ -228,7 +228,7 @@ function getSaleIndexable( return null; // Mixed currencies not supported } - const currencyAddress = paymentItems[0].token; + const currencyAddress = paymentItems[0]!.token; const currencySymbol = getCurrencySymbol(chainId, currencyAddress); if (!currencySymbol) { return null; // Unsupported currency @@ -244,7 +244,15 @@ function getSaleIndexable( const contractAddress = nftItem.token; const tokenId = nftItem.identifier.toString(); const tokenIdHex = uint256ToHex32(BigInt(tokenId)); - const domainId = getDomainIdByTokenId(chainId, config.namespace, contractAddress, tokenIdHex); + + // Get domain ID + let domainId; + try { + domainId = getDomainIdByTokenId(chainId, config.namespace, contractAddress, tokenIdHex); + } catch (e) { + // should we log here? + return null; + } return { ...sharedEventValues(context.chain.id, event), diff --git a/apps/ensindexer/src/lib/tokenscope-helpers.ts b/apps/ensindexer/src/lib/tokenscope-helpers.ts new file mode 100644 index 000000000..721367191 --- /dev/null +++ b/apps/ensindexer/src/lib/tokenscope-helpers.ts @@ -0,0 +1,397 @@ +import { + DatasourceNames, + ENSNamespaceId, + ENSNamespaceIds, + getDatasource, +} from "@ensnode/datasources"; +import {BASE_NODE, ChainId, ETH_NODE, makeSubdomainNode} from "@ensnode/ensnode-sdk"; +import { Address, Hex } from "viem"; +import { + base, + baseSepolia, + holesky, + linea, + lineaSepolia, + mainnet, + optimism, + sepolia, +} from "viem/chains"; + +/** + * Identifies a specific address on a specific chain. + */ +export interface ChainAddress { + chainId: ChainId; + address: Address; +} + +/** + * Identifies a specific currency. + */ +export interface Currency { + symbol: string; + name: string; + decimals: number; + // For native currencies, address will be null + address: Address | null; +} + +/** + * Identifies a specific currency on a specific chain. + */ +export interface ChainCurrency extends Currency { + chainId: ChainId; +} + +/** + * Returns an array of 0 or more ChainAddress objects that are known to provide tokenized name ownership. + * + * @param namespaceId - The ENSNamespace identifier (e.g. 'mainnet', 'sepolia', 'holesky', 'ens-test-env') + * @returns an array of 0 or more ChainAddress objects + */ +export const getKnownTokenIssuingContracts = (namespaceId: ENSNamespaceId): ChainAddress[] => { + switch (namespaceId) { + case ENSNamespaceIds.Mainnet: { + const rootDatasource = getDatasource(namespaceId, DatasourceNames.ENSRoot); + const lineanamesDatasource = getDatasource(namespaceId, DatasourceNames.Lineanames); + const basenamesDatasource = getDatasource(namespaceId, DatasourceNames.Basenames); + const threeDnsBaseDatasource = getDatasource(namespaceId, DatasourceNames.ThreeDNSBase); + const threeDnsOptimismDatasource = getDatasource( + namespaceId, + DatasourceNames.ThreeDNSOptimism, + ); + return [ + // Eth Token - Mainnet + { + chainId: rootDatasource.chain.id, + address: rootDatasource.contracts["BaseRegistrar"].address, + }, + // NameWrapper Token - Mainnet + { + chainId: rootDatasource.chain.id, + address: rootDatasource.contracts["NameWrapper"].address, + }, + // 3DNS Token - Optimism + { + chainId: threeDnsOptimismDatasource.chain.id, + address: threeDnsOptimismDatasource.contracts["ThreeDNSToken"].address, + }, + // 3DNS Token - Base + { + chainId: threeDnsBaseDatasource.chain.id, + address: threeDnsBaseDatasource.contracts["ThreeDNSToken"].address, + }, + // Linea Names Token - Linea + { + chainId: lineanamesDatasource.chain.id, + address: lineanamesDatasource.contracts["BaseRegistrar"].address, + }, + // Base Names Token - Base + { + chainId: basenamesDatasource.chain.id, + address: basenamesDatasource.contracts["BaseRegistrar"].address, + }, + ]; + } + case ENSNamespaceIds.Sepolia: { + const rootDatasource = getDatasource(namespaceId, DatasourceNames.ENSRoot); + const basenamesDatasource = getDatasource(namespaceId, DatasourceNames.Basenames); + const lineanamesDatasource = getDatasource(namespaceId, DatasourceNames.Lineanames); + + return [ + { + // ENS Token - Sepolia + chainId: rootDatasource.chain.id, + address: rootDatasource.contracts["BaseRegistrar"].address, + }, + { + // NameWrapper Token - Sepolia + chainId: rootDatasource.chain.id, + address: rootDatasource.contracts["NameWrapper"].address, + }, + { + // Basenames Token - Base Sepolia + chainId: basenamesDatasource.chain.id, + address: basenamesDatasource.contracts["BaseRegistrar"].address, + }, + { + // Lineanames Token - Linea Sepolia + chainId: lineanamesDatasource.chain.id, + address: lineanamesDatasource.contracts["BaseRegistrar"].address, + }, + ]; + } + case ENSNamespaceIds.Holesky: { + const rootDatasource = getDatasource(namespaceId, DatasourceNames.ENSRoot); + return [ + { + // ENS Token - Holesky + chainId: rootDatasource.chain.id, + address: rootDatasource.contracts["BaseRegistrar"].address, + }, + { + // NameWrapper Token - Holesky + chainId: rootDatasource.chain.id, + address: rootDatasource.contracts["NameWrapper"].address, + }, + ]; + } + case ENSNamespaceIds.EnsTestEnv: { + const rootDatasource = getDatasource(namespaceId, DatasourceNames.ENSRoot); + return [ + { + // ENS Token - EnsTestEnv + chainId: rootDatasource.chain.id, + address: rootDatasource.contracts["BaseRegistrar"].address, + }, + { + // NameWrapper Token - EnsTestEnv + chainId: rootDatasource.chain.id, + address: rootDatasource.contracts["NameWrapper"].address, + }, + ]; + } + } +}; + +/** + * Returns a boolean indicating whether the provided ChainAddress is a known token issuing contract. + * + * @param namespaceId - The ENSNamespace identifier (e.g. 'mainnet', 'sepolia', 'holesky', 'ens-test-env') + * @param chainAddress - The ChainAddress to check + * @returns a boolean indicating whether the provided ChainAddress is a known token issuing contract + */ +export const isKnownTokenIssuingContract = ( + namespaceId: ENSNamespaceId, + chainAddress: ChainAddress, +): boolean => { + const knownContracts = getKnownTokenIssuingContracts(namespaceId); + return knownContracts.some((contract) => isEqualChainAddress(contract, chainAddress)); +}; + +/** + * Returns a boolean indicating whether the provided ChainAddress objects are equal. + * + * @param address1 - The first ChainAddress to compare + * @param address2 - The second ChainAddress to compare + * @returns a boolean indicating whether the provided ChainAddress objects are equal + */ +export const isEqualChainAddress = (address1: ChainAddress, address2: ChainAddress): boolean => { + return ( + address1.chainId === address2.chainId && + address1.address.toLowerCase() === address2.address.toLowerCase() + ); +}; + +/** + * Get the domainId by contract address and tokenId + * @param chainId - The chainId of the NFT + * @param namespaceId - The ENSNamespace identifier (e.g. 'mainnet', 'sepolia', 'holesky', 'ens-test-env') + * @param contractAddress - contract address of the NFT + * @param tokenIdHex - tokenId of the NFT in hex + */ +export function getDomainIdByTokenId( + chainId: ChainId, + namespaceId: ENSNamespaceId, + contractAddress: Address, + tokenIdHex: Hex, +): Hex { + const ensDataSource = getDatasource(namespaceId, DatasourceNames.ENSRoot); + if (ensDataSource.chain.id !== chainId) { + throw new Error(`Namespace ${namespaceId} is not deployed on chain ${chainId}`); + } + const baseRegistrarContractAddress = ensDataSource.contracts["BaseRegistrar"].address; + + // OLD ENS Registry: tokenId is labelhash so need to convert to namehash + if (contractAddress === baseRegistrarContractAddress) { + return makeSubdomainNode(tokenIdHex, ETH_NODE); + } + + const baseNamesDataSource = getDatasource(namespaceId, DatasourceNames.Basenames); + if (baseNamesDataSource.chain.id !== chainId) { + throw new Error(`Namespace ${namespaceId} is not deployed on chain ${chainId}`); + } + const basenamesContractAddress = baseNamesDataSource.contracts["BaseRegistrar"].address; + + // basenames: tokenId is labelhash so need to convert to namehash + if (contractAddress === basenamesContractAddress) { + return makeSubdomainNode(tokenIdHex, BASE_NODE); + } + + // 3dns token id is already derived from namehash + // linea token id is already derived from namehash + return tokenIdHex; +} + +// Well-known currencies +const ETH_CURRENCY = { + symbol: "ETH", + name: "Ethereum", + decimals: 18, + address: null, +} as const; + +const CHAIN_CURRENCIES = { + // Mainnet + [mainnet.id]: [ + { + symbol: "USDC", + name: "USD Coin", + decimals: 6, + address: "0xA0b86a33E6417c5Dd4Baf8C54e5de49E293E9169" as Address, + }, + { + symbol: "DAI", + name: "Dai Stablecoin", + decimals: 18, + address: "0x6B175474E89094C44Da98b954EedeAC495271d0F" as Address, + }, + ], + // Base + [base.id]: [ + { + symbol: "USDC", + name: "USD Coin", + decimals: 6, + address: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913" as Address, + }, + { + symbol: "DAI", + name: "Dai Stablecoin", + decimals: 18, + address: "0x50c5725949A6F0c72E6C4a641F24049A917DB0Cb" as Address, + }, + ], + // Optimism + [optimism.id]: [ + { + symbol: "USDC", + name: "USD Coin", + decimals: 6, + address: "0x0b2C639c533813f4Aa9D7837CAf62653d097Ff85" as Address, + }, + { + symbol: "DAI", + name: "Dai Stablecoin", + decimals: 18, + address: "0xDA10009cBd5D07dd0CeCc66161FC93D7c9000da1" as Address, + }, + ], + // Linea + [linea.id]: [ + { + symbol: "USDC", + name: "USD Coin", + decimals: 6, + address: "0x176211869cA2b568f2A7D4EE941E073a821EE1ff" as Address, + }, + { + symbol: "DAI", + name: "Dai Stablecoin", + decimals: 18, + address: "0x4AF15ec2A0BD43Db75dd04E62FAA3B8EF36b00d5" as Address, + }, + ], + // Sepolia + [sepolia.id]: [ + { + symbol: "USDC", + name: "USD Coin", + decimals: 6, + address: "0x1c7D4B196Cb0C7B01d743Fbc6116a902379C7238" as Address, + }, + { + symbol: "DAI", + name: "Dai Stablecoin", + decimals: 18, + address: "0x3e622317f8C93f7328350cF0B56d9eD4C620C5d6" as Address, + }, + ], + // Holesky + [holesky.id]: [ + { + symbol: "USDC", + name: "USD Coin", + decimals: 6, + address: "0x1c7D4B196Cb0C7B01d743Fbc6116a902379C7238" as Address, + }, + { + symbol: "DAI", + name: "Dai Stablecoin", + decimals: 18, + address: "0x3e622317f8C93f7328350cF0B56d9eD4C620C5d6" as Address, + }, + ], + // Base Sepolia + [baseSepolia.id]: [ + { + symbol: "USDC", + name: "USD Coin", + decimals: 6, + address: "0x036CbD53842c5426634e7929541eC2318f3dCF7e" as Address, + }, + { + symbol: "DAI", + name: "Dai Stablecoin", + decimals: 18, + address: "0x7368C6C68a4b2b68F90DB2e8F5E3b8E1E5e4F5c7" as Address, + }, + ], + // Linea Sepolia + [lineaSepolia.id]: [ + { + symbol: "USDC", + name: "USD Coin", + decimals: 6, + address: "0x176211869cA2b568f2A7D4EE941E073a821EE1ff" as Address, + }, + { + symbol: "DAI", + name: "Dai Stablecoin", + decimals: 18, + address: "0x4AF15ec2A0BD43Db75dd04E62FAA3B8EF36b00d5" as Address, + }, + ], +} as const; + +/** + * Returns an array of supported currencies for a given chain ID. + * + * @param chainId - The chain ID to get supported currencies for + * @returns an array of ChainCurrency objects representing supported currencies on the chain + */ +export const getSupportedCurrencies = (chainId: ChainId): ChainCurrency[] => { + const chainCurrencies = CHAIN_CURRENCIES[chainId as keyof typeof CHAIN_CURRENCIES] || []; + + // Always add ETH as the native currency + const currencies: ChainCurrency[] = [ + { + ...ETH_CURRENCY, + chainId, + }, + ]; + + // Add chain-specific currencies + currencies.push( + ...chainCurrencies.map((currency) => ({ + ...currency, + chainId, + })), + ); + + return currencies; +}; + +/** + * Returns a boolean indicating whether the provided address is a known supported currency contract. + * + * @param chainId - The chain ID + * @param address - The contract address to check + * @returns a boolean indicating whether the address is a known supported currency contract + */ +export const isKnownCurrencyContract = (chainId: ChainId, address: Address): boolean => { + const supportedCurrencies = getSupportedCurrencies(chainId); + return supportedCurrencies.some( + (currency) => currency.address && currency.address.toLowerCase() === address.toLowerCase(), + ); +}; diff --git a/packages/ensnode-sdk/src/ens/constants.ts b/packages/ensnode-sdk/src/ens/constants.ts index c22967eb0..be53fd860 100644 --- a/packages/ensnode-sdk/src/ens/constants.ts +++ b/packages/ensnode-sdk/src/ens/constants.ts @@ -4,6 +4,7 @@ import type { Node } from "./types"; export const ROOT_NODE: Node = namehash(""); export const ETH_NODE = namehash("eth"); +export const BASE_NODE = namehash("base.eth"); /** * A set of nodes whose children are used for reverse resolution. From 956d9a2d282c896c917fee4b3a576fdde48f7497 Mon Sep 17 00:00:00 2001 From: "lightwalker.eth" <126201998+lightwalker-eth@users.noreply.github.com> Date: Mon, 25 Aug 2025 09:31:12 +0300 Subject: [PATCH 05/61] remove unnecessary context param --- apps/ensindexer/src/handlers/Seaport.ts | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/apps/ensindexer/src/handlers/Seaport.ts b/apps/ensindexer/src/handlers/Seaport.ts index f7a4a2d28..4eabedbc5 100644 --- a/apps/ensindexer/src/handlers/Seaport.ts +++ b/apps/ensindexer/src/handlers/Seaport.ts @@ -173,7 +173,6 @@ function isPaymentToken(item: OfferItem | ConsiderationItem): boolean { * and extracts the sale data if so. */ function getSaleIndexable( - context: Context, event: SeaportOrderFulfilledEvent, chainId: ChainId, ): NameSoldInsert | null { @@ -255,7 +254,7 @@ function getSaleIndexable( } return { - ...sharedEventValues(context.chain.id, event), + ...sharedEventValues(chainId, event), logIndex: event.log.logIndex, chainId, orderHash, @@ -293,9 +292,7 @@ export async function handleOrderFulfilled({ context: Context; event: SeaportOrderFulfilledEvent; }) { - const chainId = context.chain.id; - - const indexableSale = getSaleIndexable(context, event, chainId); + const indexableSale = getSaleIndexable(event, context.chain.id); if (indexableSale) { await handleSale(context, indexableSale); } From 1dc40ea3655b7f71e38d8e864488e54e10699aa1 Mon Sep 17 00:00:00 2001 From: "lightwalker.eth" <126201998+lightwalker-eth@users.noreply.github.com> Date: Mon, 25 Aug 2025 09:31:26 +0300 Subject: [PATCH 06/61] lint --- apps/ensindexer/src/lib/tokenscope-helpers.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/ensindexer/src/lib/tokenscope-helpers.ts b/apps/ensindexer/src/lib/tokenscope-helpers.ts index 721367191..80960ecdd 100644 --- a/apps/ensindexer/src/lib/tokenscope-helpers.ts +++ b/apps/ensindexer/src/lib/tokenscope-helpers.ts @@ -4,7 +4,7 @@ import { ENSNamespaceIds, getDatasource, } from "@ensnode/datasources"; -import {BASE_NODE, ChainId, ETH_NODE, makeSubdomainNode} from "@ensnode/ensnode-sdk"; +import { BASE_NODE, ChainId, ETH_NODE, makeSubdomainNode } from "@ensnode/ensnode-sdk"; import { Address, Hex } from "viem"; import { base, From 115f41f4761798cef2f8ca0740bf56df1ee6f991 Mon Sep 17 00:00:00 2001 From: "lightwalker.eth" <126201998+lightwalker-eth@users.noreply.github.com> Date: Mon, 25 Aug 2025 09:34:23 +0300 Subject: [PATCH 07/61] Rename function --- apps/ensindexer/src/handlers/Seaport.ts | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/apps/ensindexer/src/handlers/Seaport.ts b/apps/ensindexer/src/handlers/Seaport.ts index 4eabedbc5..2b19c8c1b 100644 --- a/apps/ensindexer/src/handlers/Seaport.ts +++ b/apps/ensindexer/src/handlers/Seaport.ts @@ -169,10 +169,12 @@ function isPaymentToken(item: OfferItem | ConsiderationItem): boolean { } /** - * Determines if a Seaport order fulfillment represents an indexable sale - * and extracts the sale data if so. + * Determines if a Seaport order fulfillment meets our criteria for indexing. + * + * @returns indexable sale data, or `null` if the order fulfilled event does + * not meet our criteria for indexing. */ -function getSaleIndexable( +function getIndexableSale( event: SeaportOrderFulfilledEvent, chainId: ChainId, ): NameSoldInsert | null { @@ -292,7 +294,7 @@ export async function handleOrderFulfilled({ context: Context; event: SeaportOrderFulfilledEvent; }) { - const indexableSale = getSaleIndexable(event, context.chain.id); + const indexableSale = getIndexableSale(event, context.chain.id); if (indexableSale) { await handleSale(context, indexableSale); } From 9cbf5369e420bc878d8bc042fc0909486d7516ff Mon Sep 17 00:00:00 2001 From: "lightwalker.eth" <126201998+lightwalker-eth@users.noreply.github.com> Date: Mon, 25 Aug 2025 09:36:30 +0300 Subject: [PATCH 08/61] Fix chainId type --- apps/ensindexer/src/handlers/Seaport.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/ensindexer/src/handlers/Seaport.ts b/apps/ensindexer/src/handlers/Seaport.ts index 2b19c8c1b..7ac1b41eb 100644 --- a/apps/ensindexer/src/handlers/Seaport.ts +++ b/apps/ensindexer/src/handlers/Seaport.ts @@ -151,7 +151,7 @@ function getCurrencySymbol(chainId: number, currencyAddress: Address): string | /** * Checks if an item is a supported NFT (ERC721/ERC1155 from known contracts) */ -function isSupportedNFT(chainId: number, item: OfferItem | ConsiderationItem): boolean { +function isSupportedNFT(chainId: ChainId, item: OfferItem | ConsiderationItem): boolean { const isValidItemType = item.itemType === ItemType.ERC721 || item.itemType === ItemType.ERC1155; const isSupportedContract = isKnownTokenIssuingContract(config.namespace, { chainId, From fa074d72f3afc00b5a2ded8ff3eef49e2efb7777 Mon Sep 17 00:00:00 2001 From: "lightwalker.eth" <126201998+lightwalker-eth@users.noreply.github.com> Date: Mon, 25 Aug 2025 10:11:17 +0300 Subject: [PATCH 09/61] Move TokenType definition --- apps/ensindexer/src/handlers/Seaport.ts | 3 ++- apps/ensindexer/src/lib/tokenscope-helpers.ts | 12 ++++++++++++ packages/ensnode-schema/src/tokenscope.schema.ts | 9 +-------- 3 files changed, 15 insertions(+), 9 deletions(-) diff --git a/apps/ensindexer/src/handlers/Seaport.ts b/apps/ensindexer/src/handlers/Seaport.ts index 7ac1b41eb..f38822679 100644 --- a/apps/ensindexer/src/handlers/Seaport.ts +++ b/apps/ensindexer/src/handlers/Seaport.ts @@ -6,11 +6,12 @@ import config from "@/config"; import { sharedEventValues, upsertAccount } from "@/lib/db-helpers"; import { EventWithArgs } from "@/lib/ponder-helpers"; import { + TokenTypes, getDomainIdByTokenId, getSupportedCurrencies, isKnownTokenIssuingContract, } from "@/lib/tokenscope-helpers"; -import { NameSoldInsert, TokenTypes } from "@ensnode/ensnode-schema"; +import { NameSoldInsert } from "@ensnode/ensnode-schema"; import { ChainId, uint256ToHex32 } from "@ensnode/ensnode-sdk"; import { Address, Hex, zeroAddress } from "viem"; diff --git a/apps/ensindexer/src/lib/tokenscope-helpers.ts b/apps/ensindexer/src/lib/tokenscope-helpers.ts index 80960ecdd..1e500b03f 100644 --- a/apps/ensindexer/src/lib/tokenscope-helpers.ts +++ b/apps/ensindexer/src/lib/tokenscope-helpers.ts @@ -25,6 +25,18 @@ export interface ChainAddress { address: Address; } +export const TokenTypes = { + ERC721: "ERC721", + ERC1155: "ERC1155", +} as const; + +export type TokenType = (typeof TokenTypes)[keyof typeof TokenTypes]; + +/** + * A uint256 value that identifies a specific token within a NFT contract. + */ +export type TokenId = bigint; + /** * Identifies a specific currency. */ diff --git a/packages/ensnode-schema/src/tokenscope.schema.ts b/packages/ensnode-schema/src/tokenscope.schema.ts index 97f2fb5fb..1e193e7d6 100644 --- a/packages/ensnode-schema/src/tokenscope.schema.ts +++ b/packages/ensnode-schema/src/tokenscope.schema.ts @@ -1,13 +1,6 @@ import schema from "ponder:schema"; import { index, onchainTable } from "ponder"; -export const TokenTypes = { - ERC721: "ERC721", - ERC1155: "ERC1155", -} as const; - -export type TokenType = (typeof TokenTypes)[keyof typeof TokenTypes]; - const sharedEventColumns = (t: any) => ({ /** * The unique identifier of the event. @@ -121,7 +114,7 @@ export const nameSold = onchainTable( /** * The type of token being sold (ERC721 or ERC1155). */ - tokenType: t.text().notNull().$type(), + tokenType: t.text().notNull(), }), (t) => ({ idx_from: index().on(t.fromOwnerId), From 783b9c108dc7675d19b9d2cb4583744255ac6c15 Mon Sep 17 00:00:00 2001 From: "lightwalker.eth" <126201998+lightwalker-eth@users.noreply.github.com> Date: Mon, 25 Aug 2025 10:14:43 +0300 Subject: [PATCH 10/61] Refine isEqualChainAddress --- apps/ensindexer/src/lib/tokenscope-helpers.ts | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/apps/ensindexer/src/lib/tokenscope-helpers.ts b/apps/ensindexer/src/lib/tokenscope-helpers.ts index 1e500b03f..d09283a61 100644 --- a/apps/ensindexer/src/lib/tokenscope-helpers.ts +++ b/apps/ensindexer/src/lib/tokenscope-helpers.ts @@ -5,7 +5,7 @@ import { getDatasource, } from "@ensnode/datasources"; import { BASE_NODE, ChainId, ETH_NODE, makeSubdomainNode } from "@ensnode/ensnode-sdk"; -import { Address, Hex } from "viem"; +import { Address, Hex, isAddressEqual } from "viem"; import { base, baseSepolia, @@ -184,15 +184,12 @@ export const isKnownTokenIssuingContract = ( /** * Returns a boolean indicating whether the provided ChainAddress objects are equal. * - * @param address1 - The first ChainAddress to compare - * @param address2 - The second ChainAddress to compare + * @param ca1 - The first ChainAddress to compare + * @param ca2 - The second ChainAddress to compare * @returns a boolean indicating whether the provided ChainAddress objects are equal */ -export const isEqualChainAddress = (address1: ChainAddress, address2: ChainAddress): boolean => { - return ( - address1.chainId === address2.chainId && - address1.address.toLowerCase() === address2.address.toLowerCase() - ); +export const isEqualChainAddress = (ca1: ChainAddress, ca2: ChainAddress): boolean => { + return ca1.chainId === ca2.chainId && isAddressEqual(ca1.address, ca2.address); }; /** From 5a0643a4e456e0613f935acb0863441ced83b853 Mon Sep 17 00:00:00 2001 From: "lightwalker.eth" <126201998+lightwalker-eth@users.noreply.github.com> Date: Mon, 25 Aug 2025 12:20:34 +0300 Subject: [PATCH 11/61] Refactor getDomainIdByTokenId and related --- apps/ensindexer/src/handlers/Seaport.ts | 19 +- apps/ensindexer/src/lib/tokenscope-helpers.ts | 331 +++++++++--------- packages/datasources/src/lib/types.ts | 29 +- packages/datasources/src/namespaces.ts | 30 ++ packages/ensnode-sdk/src/ens/constants.ts | 3 +- .../ensnode-sdk/src/ens/subname-helpers.ts | 4 +- .../ensnode-sdk/src/shared/deserialize.ts | 3 +- packages/ensnode-sdk/src/shared/serialize.ts | 3 +- .../src/shared/serialized-types.ts | 2 - packages/ensnode-sdk/src/shared/types.ts | 8 - .../ensnode-sdk/src/shared/zod-schemas.ts | 2 +- 11 files changed, 249 insertions(+), 185 deletions(-) diff --git a/apps/ensindexer/src/handlers/Seaport.ts b/apps/ensindexer/src/handlers/Seaport.ts index f38822679..0d60c7e23 100644 --- a/apps/ensindexer/src/handlers/Seaport.ts +++ b/apps/ensindexer/src/handlers/Seaport.ts @@ -11,8 +11,8 @@ import { getSupportedCurrencies, isKnownTokenIssuingContract, } from "@/lib/tokenscope-helpers"; +import { ChainAddress, ChainId } from "@ensnode/datasources"; import { NameSoldInsert } from "@ensnode/ensnode-schema"; -import { ChainId, uint256ToHex32 } from "@ensnode/ensnode-sdk"; import { Address, Hex, zeroAddress } from "viem"; type OfferItem = { @@ -243,16 +243,19 @@ function getIndexableSale( } // Extract NFT details - const contractAddress = nftItem.token; const tokenId = nftItem.identifier.toString(); - const tokenIdHex = uint256ToHex32(BigInt(tokenId)); + + const contract: ChainAddress = { + chainId, + address: nftItem.token, + }; // Get domain ID let domainId; try { - domainId = getDomainIdByTokenId(chainId, config.namespace, contractAddress, tokenIdHex); - } catch (e) { - // should we log here? + domainId = getDomainIdByTokenId(config.namespace, contract, nftItem.identifier); + } catch { + // unsupported NFT contract return null; } @@ -264,8 +267,8 @@ function getIndexableSale( timestamp: event.block.timestamp, fromOwnerId: seller, newOwnerId: buyer, - contractAddress: contractAddress, - tokenId: tokenId, + contractAddress: contract.address, + tokenId, tokenType: nftItem.itemType === ItemType.ERC721 ? TokenTypes.ERC721 : TokenTypes.ERC1155, domainId, currency: currencySymbol, diff --git a/apps/ensindexer/src/lib/tokenscope-helpers.ts b/apps/ensindexer/src/lib/tokenscope-helpers.ts index d09283a61..f7bb74203 100644 --- a/apps/ensindexer/src/lib/tokenscope-helpers.ts +++ b/apps/ensindexer/src/lib/tokenscope-helpers.ts @@ -1,11 +1,21 @@ import { + ChainAddress, + ChainId, DatasourceNames, ENSNamespaceId, - ENSNamespaceIds, - getDatasource, + isChainAddressEqual, + maybeGetDatasourceContractChainAddress, } from "@ensnode/datasources"; -import { BASE_NODE, ChainId, ETH_NODE, makeSubdomainNode } from "@ensnode/ensnode-sdk"; -import { Address, Hex, isAddressEqual } from "viem"; +import { + BASENAMES_NODE, + ETH_NODE, + LINEANAMES_NODE, + LabelHash, + type Node, + makeSubdomainNode, + uint256ToHex32, +} from "@ensnode/ensnode-sdk"; +import { Address } from "viem"; import { base, baseSepolia, @@ -17,14 +27,6 @@ import { sepolia, } from "viem/chains"; -/** - * Identifies a specific address on a specific chain. - */ -export interface ChainAddress { - chainId: ChainId; - address: Address; -} - export const TokenTypes = { ERC721: "ERC721", ERC1155: "ERC1155", @@ -56,180 +58,189 @@ export interface ChainCurrency extends Currency { } /** - * Returns an array of 0 or more ChainAddress objects that are known to provide tokenized name ownership. + * A contract that issues tokenized ENS names. + */ +export interface TokenIssuingContract { + /** + * The ChainAddress of the token issuing contract. + */ + contract: ChainAddress; + + /** + * Applies the contract's logic for converting from the token id + * representation of a name to the domain id (Node) of the name. + */ + getDomainId: (tokenId: TokenId) => Node; +} + +/** + * Converts the tokenId from an ENS name token-issuing contract to a Node + * for the case that the contract generates each tokenId using namehash of + * the full name. + * + * @param tokenId - The tokenId to convert + * @returns The Node of the tokenId + */ +export const nameHashGeneratedTokenIdToNode = (tokenId: TokenId): Node => { + return uint256ToHex32(tokenId); +}; + +/** + * Converts the tokenId from an ENS name token-issuing contract to a Node + * for the case that the contract generates each tokenId using labelhash of + * the direct subname of the parent node. + * + * @param tokenId - The tokenId to convert + * @param parentNode - the parent Node that the token issuing contract issues subnames under + * @returns The Node of the tokenId issued under the parentNode + */ +export const labelHashGeneratedTokenIdToNode = (tokenId: TokenId, parentNode: Node): Node => { + const labelHash: LabelHash = uint256ToHex32(tokenId); + return makeSubdomainNode(labelHash, parentNode); +}; + +/** + * Gets the contracts known to provide tokenized name ownership within the + * specified namespace. * * @param namespaceId - The ENSNamespace identifier (e.g. 'mainnet', 'sepolia', 'holesky', 'ens-test-env') - * @returns an array of 0 or more ChainAddress objects + * @returns an array of 0 or more known TokenIssuingContract for the specified namespace */ -export const getKnownTokenIssuingContracts = (namespaceId: ENSNamespaceId): ChainAddress[] => { - switch (namespaceId) { - case ENSNamespaceIds.Mainnet: { - const rootDatasource = getDatasource(namespaceId, DatasourceNames.ENSRoot); - const lineanamesDatasource = getDatasource(namespaceId, DatasourceNames.Lineanames); - const basenamesDatasource = getDatasource(namespaceId, DatasourceNames.Basenames); - const threeDnsBaseDatasource = getDatasource(namespaceId, DatasourceNames.ThreeDNSBase); - const threeDnsOptimismDatasource = getDatasource( - namespaceId, - DatasourceNames.ThreeDNSOptimism, - ); - return [ - // Eth Token - Mainnet - { - chainId: rootDatasource.chain.id, - address: rootDatasource.contracts["BaseRegistrar"].address, - }, - // NameWrapper Token - Mainnet - { - chainId: rootDatasource.chain.id, - address: rootDatasource.contracts["NameWrapper"].address, - }, - // 3DNS Token - Optimism - { - chainId: threeDnsOptimismDatasource.chain.id, - address: threeDnsOptimismDatasource.contracts["ThreeDNSToken"].address, - }, - // 3DNS Token - Base - { - chainId: threeDnsBaseDatasource.chain.id, - address: threeDnsBaseDatasource.contracts["ThreeDNSToken"].address, - }, - // Linea Names Token - Linea - { - chainId: lineanamesDatasource.chain.id, - address: lineanamesDatasource.contracts["BaseRegistrar"].address, - }, - // Base Names Token - Base - { - chainId: basenamesDatasource.chain.id, - address: basenamesDatasource.contracts["BaseRegistrar"].address, - }, - ]; - } - case ENSNamespaceIds.Sepolia: { - const rootDatasource = getDatasource(namespaceId, DatasourceNames.ENSRoot); - const basenamesDatasource = getDatasource(namespaceId, DatasourceNames.Basenames); - const lineanamesDatasource = getDatasource(namespaceId, DatasourceNames.Lineanames); +export const getKnownTokenIssuingContracts = ( + namespaceId: ENSNamespaceId, +): TokenIssuingContract[] => { + const ethBaseRegistrar = maybeGetDatasourceContractChainAddress( + namespaceId, + DatasourceNames.ENSRoot, + "BaseRegistrar", + ); + const nameWrapper = maybeGetDatasourceContractChainAddress( + namespaceId, + DatasourceNames.ENSRoot, + "NameWrapper", + ); + const threeDnsBaseRegistrar = maybeGetDatasourceContractChainAddress( + namespaceId, + DatasourceNames.ThreeDNSBase, + "ThreeDNSToken", + ); + const threeDnsOptimismRegistrar = maybeGetDatasourceContractChainAddress( + namespaceId, + DatasourceNames.ThreeDNSOptimism, + "ThreeDNSToken", + ); + const lineanamesRegistrar = maybeGetDatasourceContractChainAddress( + namespaceId, + DatasourceNames.Lineanames, + "BaseRegistrar", + ); + const basenamesRegistrar = maybeGetDatasourceContractChainAddress( + namespaceId, + DatasourceNames.Basenames, + "BaseRegistrar", + ); + + const result: TokenIssuingContract[] = []; + + if (ethBaseRegistrar) { + result.push({ + contract: ethBaseRegistrar, + getDomainId: (tokenId: TokenId): Node => { + return labelHashGeneratedTokenIdToNode(tokenId, ETH_NODE); + }, + }); + } + + if (nameWrapper) { + result.push({ + contract: nameWrapper, + getDomainId: (tokenId: TokenId): Node => { + return nameHashGeneratedTokenIdToNode(tokenId); + }, + }); + } + + if (threeDnsBaseRegistrar) { + result.push({ + contract: threeDnsBaseRegistrar, + getDomainId: (tokenId: TokenId): Node => { + return nameHashGeneratedTokenIdToNode(tokenId); + }, + }); + } + + if (threeDnsOptimismRegistrar) { + result.push({ + contract: threeDnsOptimismRegistrar, + getDomainId: (tokenId: TokenId): Node => { + return nameHashGeneratedTokenIdToNode(tokenId); + }, + }); + } + + if (lineanamesRegistrar) { + result.push({ + contract: lineanamesRegistrar, + getDomainId: (tokenId: TokenId): Node => { + return labelHashGeneratedTokenIdToNode(tokenId, LINEANAMES_NODE); + }, + }); + } - return [ - { - // ENS Token - Sepolia - chainId: rootDatasource.chain.id, - address: rootDatasource.contracts["BaseRegistrar"].address, - }, - { - // NameWrapper Token - Sepolia - chainId: rootDatasource.chain.id, - address: rootDatasource.contracts["NameWrapper"].address, - }, - { - // Basenames Token - Base Sepolia - chainId: basenamesDatasource.chain.id, - address: basenamesDatasource.contracts["BaseRegistrar"].address, - }, - { - // Lineanames Token - Linea Sepolia - chainId: lineanamesDatasource.chain.id, - address: lineanamesDatasource.contracts["BaseRegistrar"].address, - }, - ]; - } - case ENSNamespaceIds.Holesky: { - const rootDatasource = getDatasource(namespaceId, DatasourceNames.ENSRoot); - return [ - { - // ENS Token - Holesky - chainId: rootDatasource.chain.id, - address: rootDatasource.contracts["BaseRegistrar"].address, - }, - { - // NameWrapper Token - Holesky - chainId: rootDatasource.chain.id, - address: rootDatasource.contracts["NameWrapper"].address, - }, - ]; - } - case ENSNamespaceIds.EnsTestEnv: { - const rootDatasource = getDatasource(namespaceId, DatasourceNames.ENSRoot); - return [ - { - // ENS Token - EnsTestEnv - chainId: rootDatasource.chain.id, - address: rootDatasource.contracts["BaseRegistrar"].address, - }, - { - // NameWrapper Token - EnsTestEnv - chainId: rootDatasource.chain.id, - address: rootDatasource.contracts["NameWrapper"].address, - }, - ]; - } + if (basenamesRegistrar) { + result.push({ + contract: basenamesRegistrar, + getDomainId: (tokenId: TokenId): Node => { + return labelHashGeneratedTokenIdToNode(tokenId, BASENAMES_NODE); + }, + }); } + + return result; }; /** - * Returns a boolean indicating whether the provided ChainAddress is a known token issuing contract. + * Identifies if the provided ChainAddress is a known token issuing contract. * * @param namespaceId - The ENSNamespace identifier (e.g. 'mainnet', 'sepolia', 'holesky', 'ens-test-env') * @param chainAddress - The ChainAddress to check - * @returns a boolean indicating whether the provided ChainAddress is a known token issuing contract + * @returns a boolean indicating if the provided ChainAddress is a known token issuing contract */ export const isKnownTokenIssuingContract = ( namespaceId: ENSNamespaceId, chainAddress: ChainAddress, ): boolean => { - const knownContracts = getKnownTokenIssuingContracts(namespaceId); - return knownContracts.some((contract) => isEqualChainAddress(contract, chainAddress)); + const knownTokenIssuingContracts = getKnownTokenIssuingContracts(namespaceId); + return knownTokenIssuingContracts.some((tokenIssuingContract) => + isChainAddressEqual(tokenIssuingContract.contract, chainAddress), + ); }; /** - * Returns a boolean indicating whether the provided ChainAddress objects are equal. + * Gets the domainId (Node) for a given NFT from contract with tokenId on the specified namespace. * - * @param ca1 - The first ChainAddress to compare - * @param ca2 - The second ChainAddress to compare - * @returns a boolean indicating whether the provided ChainAddress objects are equal - */ -export const isEqualChainAddress = (ca1: ChainAddress, ca2: ChainAddress): boolean => { - return ca1.chainId === ca2.chainId && isAddressEqual(ca1.address, ca2.address); -}; - -/** - * Get the domainId by contract address and tokenId - * @param chainId - The chainId of the NFT * @param namespaceId - The ENSNamespace identifier (e.g. 'mainnet', 'sepolia', 'holesky', 'ens-test-env') - * @param contractAddress - contract address of the NFT - * @param tokenIdHex - tokenId of the NFT in hex + * @param contract - The ChainAddress of the NFT contract + * @param tokenId - The tokenId of the NFT + * @returns the domainId (Node) for ENS name associated with the NFT + * @throws an error if the contract is not a known token issuing contract in the specified namespace */ export function getDomainIdByTokenId( - chainId: ChainId, namespaceId: ENSNamespaceId, - contractAddress: Address, - tokenIdHex: Hex, -): Hex { - const ensDataSource = getDatasource(namespaceId, DatasourceNames.ENSRoot); - if (ensDataSource.chain.id !== chainId) { - throw new Error(`Namespace ${namespaceId} is not deployed on chain ${chainId}`); - } - const baseRegistrarContractAddress = ensDataSource.contracts["BaseRegistrar"].address; - - // OLD ENS Registry: tokenId is labelhash so need to convert to namehash - if (contractAddress === baseRegistrarContractAddress) { - return makeSubdomainNode(tokenIdHex, ETH_NODE); - } - - const baseNamesDataSource = getDatasource(namespaceId, DatasourceNames.Basenames); - if (baseNamesDataSource.chain.id !== chainId) { - throw new Error(`Namespace ${namespaceId} is not deployed on chain ${chainId}`); - } - const basenamesContractAddress = baseNamesDataSource.contracts["BaseRegistrar"].address; - - // basenames: tokenId is labelhash so need to convert to namehash - if (contractAddress === basenamesContractAddress) { - return makeSubdomainNode(tokenIdHex, BASE_NODE); + contract: ChainAddress, + tokenId: TokenId, +): Node { + const knownTokenIssuingContracts = getKnownTokenIssuingContracts(namespaceId); + const knownTokenIssuingContract = knownTokenIssuingContracts.find((tokenIssuingContract) => + isChainAddressEqual(tokenIssuingContract.contract, contract), + ); + if (!knownTokenIssuingContract) { + throw new Error( + `The contract at address ${contract.address} on chain ${contract.chainId} is not a known token issuing contract in the ${namespaceId} namespace`, + ); } - // 3dns token id is already derived from namehash - // linea token id is already derived from namehash - return tokenIdHex; + return knownTokenIssuingContract.getDomainId(tokenId); } // Well-known currencies diff --git a/packages/datasources/src/lib/types.ts b/packages/datasources/src/lib/types.ts index 56f239ec7..0d6f3e99d 100644 --- a/packages/datasources/src/lib/types.ts +++ b/packages/datasources/src/lib/types.ts @@ -1,4 +1,4 @@ -import type { Abi, Address, Chain } from "viem"; +import { type Abi, type Address, type Chain, isAddressEqual } from "viem"; /** * ENSNamespaceIds encodes the set of identifiers for well-known ENS namespaces. @@ -121,3 +121,30 @@ export type ContractConfig = export type ENSNamespace = { [DatasourceNames.ENSRoot]: Datasource; } & Partial, Datasource>>; + +/** + * Chain ID + * + * Represents a unique identifier for a chain. + * Guaranteed to be a positive integer. + **/ +export type ChainId = number; + +/** + * Identifies a specific address on a specific chain. + */ +export interface ChainAddress { + chainId: ChainId; + address: Address; +} + +/** + * Identifies if the provided ChainAddress values are equal. + * + * @param ca1 - The first ChainAddress to compare + * @param ca2 - The second ChainAddress to compare + * @returns a boolean indicating if the provided ChainAddress values are equal + */ +export const isChainAddressEqual = (ca1: ChainAddress, ca2: ChainAddress): boolean => { + return ca1.chainId === ca2.chainId && isAddressEqual(ca1.address, ca2.address); +}; diff --git a/packages/datasources/src/namespaces.ts b/packages/datasources/src/namespaces.ts index 7b662aff5..004ee327a 100644 --- a/packages/datasources/src/namespaces.ts +++ b/packages/datasources/src/namespaces.ts @@ -1,6 +1,7 @@ import ensTestEnv from "./ens-test-env"; import holesky from "./holesky"; import { + ChainAddress, Datasource, DatasourceName, DatasourceNames, @@ -70,6 +71,35 @@ export const maybeGetDatasource = ( datasourceName: DatasourceName, ): Datasource | undefined => (getENSNamespace(namespaceId) as ENSNamespace)[datasourceName]; +/** + * Returns the chain address for the specified namespace, datasource, and + * contract name, or undefined if it does not exist or is not a single chain address. + * + * This is useful when you want to retrieve the ChainAddress for an arbitrary contract + * where it may or may not actually be defined. + * + * @param namespaceId - The ENSNamespace identifier (e.g. + * 'mainnet', 'sepolia', 'holesky', 'ens-test-env') + * @param datasourceName - The name of the Datasource to search for contractName in + * @param contractName - The name of the contract to retrieve the chain address for + * @returns The ChainAddress for the given namespace, datasource, and contract + * name, or undefined if it does not exist or is not a single chain address + */ +export const maybeGetDatasourceContractChainAddress = ( + namespaceId: ENSNamespaceId, + datasourceName: DatasourceName, + contractName: string, +): ChainAddress | undefined => { + const datasource = maybeGetDatasource(namespaceId, datasourceName); + if (!datasource) return undefined; + const maybeAddress = datasource.contracts[contractName]?.address; + if (maybeAddress === undefined || Array.isArray(maybeAddress)) return undefined; + return { + chainId: datasource.chain.id, + address: maybeAddress, + } satisfies ChainAddress; +}; + /** * Returns the chain for the ENS Root Datasource within the selected namespace. * diff --git a/packages/ensnode-sdk/src/ens/constants.ts b/packages/ensnode-sdk/src/ens/constants.ts index be53fd860..23b6fe02f 100644 --- a/packages/ensnode-sdk/src/ens/constants.ts +++ b/packages/ensnode-sdk/src/ens/constants.ts @@ -4,7 +4,8 @@ import type { Node } from "./types"; export const ROOT_NODE: Node = namehash(""); export const ETH_NODE = namehash("eth"); -export const BASE_NODE = namehash("base.eth"); +export const BASENAMES_NODE = namehash("base.eth"); +export const LINEANAMES_NODE = namehash("linea.eth"); /** * A set of nodes whose children are used for reverse resolution. diff --git a/packages/ensnode-sdk/src/ens/subname-helpers.ts b/packages/ensnode-sdk/src/ens/subname-helpers.ts index 80ca5de63..ac93c7b99 100644 --- a/packages/ensnode-sdk/src/ens/subname-helpers.ts +++ b/packages/ensnode-sdk/src/ens/subname-helpers.ts @@ -1,4 +1,4 @@ -import { Address, concat, isAddress, isHash, keccak256, toHex } from "viem"; +import { Address, Hex, concat, isAddress, isHash, keccak256, toHex } from "viem"; import { labelhash } from "viem/ens"; import { addrReverseLabel } from "./reverse-name"; @@ -60,7 +60,7 @@ export const maybeHealLabelByReverseAddress = ({ * into Node or LabelHash, which is a common behavior in the ENS ecosystem. * (see NameWrapper, ETHRegistrarController) */ -export const uint256ToHex32 = (num: bigint) => toHex(num, { size: 32 }); +export const uint256ToHex32 = (num: bigint): Hex => toHex(num, { size: 32 }); /** * These characters are prohibited in normalized ENS names per the ENSIP-15 diff --git a/packages/ensnode-sdk/src/shared/deserialize.ts b/packages/ensnode-sdk/src/shared/deserialize.ts index c628f3a73..e443040a3 100644 --- a/packages/ensnode-sdk/src/shared/deserialize.ts +++ b/packages/ensnode-sdk/src/shared/deserialize.ts @@ -1,6 +1,7 @@ +import { ChainId } from "@ensnode/datasources"; import { prettifyError } from "zod/v4"; import type { ChainIdString, UrlString } from "./serialized-types"; -import type { BlockNumber, BlockRef, Blockrange, ChainId, Datetime, Duration } from "./types"; +import type { BlockNumber, BlockRef, Blockrange, Datetime, Duration } from "./types"; import { makeBlockNumberSchema, makeBlockRefSchema, diff --git a/packages/ensnode-sdk/src/shared/serialize.ts b/packages/ensnode-sdk/src/shared/serialize.ts index 80faf6ba9..d2b23ef4f 100644 --- a/packages/ensnode-sdk/src/shared/serialize.ts +++ b/packages/ensnode-sdk/src/shared/serialize.ts @@ -1,5 +1,6 @@ +import { ChainId } from "@ensnode/datasources"; import type { ChainIdString, DatetimeISO8601, UrlString } from "./serialized-types"; -import type { ChainId, Datetime } from "./types"; +import type { Datetime } from "./types"; /** * Serializes a {@link ChainId} value into its string representation. diff --git a/packages/ensnode-sdk/src/shared/serialized-types.ts b/packages/ensnode-sdk/src/shared/serialized-types.ts index 783114910..f14de2ef4 100644 --- a/packages/ensnode-sdk/src/shared/serialized-types.ts +++ b/packages/ensnode-sdk/src/shared/serialized-types.ts @@ -1,5 +1,3 @@ -import type { ChainId } from "./types"; - /** * A string representation of {@link ChainId}. **/ diff --git a/packages/ensnode-sdk/src/shared/types.ts b/packages/ensnode-sdk/src/shared/types.ts index c1ba24e32..7705a73e2 100644 --- a/packages/ensnode-sdk/src/shared/types.ts +++ b/packages/ensnode-sdk/src/shared/types.ts @@ -1,11 +1,3 @@ -/** - * Chain ID - * - * Represents a unique identifier for a chain. - * Guaranteed to be a positive integer. - **/ -export type ChainId = number; - /** * Block Number * diff --git a/packages/ensnode-sdk/src/shared/zod-schemas.ts b/packages/ensnode-sdk/src/shared/zod-schemas.ts index bbee7e190..aba8a1580 100644 --- a/packages/ensnode-sdk/src/shared/zod-schemas.ts +++ b/packages/ensnode-sdk/src/shared/zod-schemas.ts @@ -8,7 +8,7 @@ */ import z from "zod/v4"; import { ENSNamespaceIds } from "../ens"; -import type { BlockRef, ChainId, Datetime, Duration, UnixTimestamp } from "./types"; +import type { BlockRef, Datetime, Duration, UnixTimestamp } from "./types"; /** * Zod `.check()` function input. From 5756db64666958f68700ee800c0488fa2808c2a2 Mon Sep 17 00:00:00 2001 From: "lightwalker.eth" <126201998+lightwalker-eth@users.noreply.github.com> Date: Mon, 25 Aug 2025 12:28:57 +0300 Subject: [PATCH 12/61] Refactor isSupportedCurrencyContract --- apps/ensindexer/src/lib/tokenscope-helpers.ts | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/apps/ensindexer/src/lib/tokenscope-helpers.ts b/apps/ensindexer/src/lib/tokenscope-helpers.ts index f7bb74203..8abaa6923 100644 --- a/apps/ensindexer/src/lib/tokenscope-helpers.ts +++ b/apps/ensindexer/src/lib/tokenscope-helpers.ts @@ -15,7 +15,7 @@ import { makeSubdomainNode, uint256ToHex32, } from "@ensnode/ensnode-sdk"; -import { Address } from "viem"; +import { Address, isAddressEqual } from "viem"; import { base, baseSepolia, @@ -403,15 +403,14 @@ export const getSupportedCurrencies = (chainId: ChainId): ChainCurrency[] => { }; /** - * Returns a boolean indicating whether the provided address is a known supported currency contract. + * Identifies if the provided ChainAddress is a supported currency contract. * - * @param chainId - The chain ID - * @param address - The contract address to check - * @returns a boolean indicating whether the address is a known supported currency contract + * @param contract - The ChainAddress of the contract to check + * @returns a boolean indicating if the provided ChainAddress is a supported currency contract */ -export const isKnownCurrencyContract = (chainId: ChainId, address: Address): boolean => { - const supportedCurrencies = getSupportedCurrencies(chainId); +export const isSupportedCurrencyContract = (contract: ChainAddress): boolean => { + const supportedCurrencies = getSupportedCurrencies(contract.chainId); return supportedCurrencies.some( - (currency) => currency.address && currency.address.toLowerCase() === address.toLowerCase(), + (currency) => currency.address && isAddressEqual(currency.address, contract.address), ); }; From d8aa7d856e5ec67616f4cdceb1a34f7daf8aaece Mon Sep 17 00:00:00 2001 From: "lightwalker.eth" <126201998+lightwalker-eth@users.noreply.github.com> Date: Mon, 25 Aug 2025 14:00:22 +0300 Subject: [PATCH 13/61] Refine currency logic --- apps/ensindexer/src/handlers/Seaport.ts | 39 +- apps/ensindexer/src/lib/tokenscope-helpers.ts | 333 +++++++++--------- packages/datasources/src/namespaces.ts | 14 + .../ensnode-schema/src/tokenscope.schema.ts | 7 +- 4 files changed, 191 insertions(+), 202 deletions(-) diff --git a/apps/ensindexer/src/handlers/Seaport.ts b/apps/ensindexer/src/handlers/Seaport.ts index 0d60c7e23..7cf00b1ad 100644 --- a/apps/ensindexer/src/handlers/Seaport.ts +++ b/apps/ensindexer/src/handlers/Seaport.ts @@ -7,8 +7,8 @@ import { sharedEventValues, upsertAccount } from "@/lib/db-helpers"; import { EventWithArgs } from "@/lib/ponder-helpers"; import { TokenTypes, + getCurrencyIdForContract, getDomainIdByTokenId, - getSupportedCurrencies, isKnownTokenIssuingContract, } from "@/lib/tokenscope-helpers"; import { ChainAddress, ChainId } from "@ensnode/datasources"; @@ -130,25 +130,6 @@ interface SeaportOrderFulfilledEvent consideration: readonly ConsiderationItem[]; }> {} -/** - * Gets the currency symbol for a given address on a specific chain. - */ -function getCurrencySymbol(chainId: number, currencyAddress: Address): string | null { - const supportedCurrencies = getSupportedCurrencies(chainId); - - if (currencyAddress === zeroAddress) { - const ethCurrency = supportedCurrencies.find((currency) => currency.address === null); - return ethCurrency?.symbol || null; - } - - const matchingCurrency = supportedCurrencies.find( - (currency) => - currency.address && currency.address.toLowerCase() === currencyAddress.toLowerCase(), - ); - - return matchingCurrency?.symbol || null; -} - /** * Checks if an item is a supported NFT (ERC721/ERC1155 from known contracts) */ @@ -230,9 +211,13 @@ function getIndexableSale( return null; // Mixed currencies not supported } - const currencyAddress = paymentItems[0]!.token; - const currencySymbol = getCurrencySymbol(chainId, currencyAddress); - if (!currencySymbol) { + const currencyContract: ChainAddress = { + chainId, + address: paymentItems[0]!.token, + }; + + const currencyId = getCurrencyIdForContract(config.namespace, currencyContract); + if (!currencyId) { return null; // Unsupported currency } @@ -245,7 +230,7 @@ function getIndexableSale( // Extract NFT details const tokenId = nftItem.identifier.toString(); - const contract: ChainAddress = { + const nftContract: ChainAddress = { chainId, address: nftItem.token, }; @@ -253,7 +238,7 @@ function getIndexableSale( // Get domain ID let domainId; try { - domainId = getDomainIdByTokenId(config.namespace, contract, nftItem.identifier); + domainId = getDomainIdByTokenId(config.namespace, nftContract, nftItem.identifier); } catch { // unsupported NFT contract return null; @@ -267,11 +252,11 @@ function getIndexableSale( timestamp: event.block.timestamp, fromOwnerId: seller, newOwnerId: buyer, - contractAddress: contract.address, + contractAddress: nftContract.address, tokenId, tokenType: nftItem.itemType === ItemType.ERC721 ? TokenTypes.ERC721 : TokenTypes.ERC1155, domainId, - currency: currencySymbol, + currencyId, price: totalAmount, }; } diff --git a/apps/ensindexer/src/lib/tokenscope-helpers.ts b/apps/ensindexer/src/lib/tokenscope-helpers.ts index 8abaa6923..46dbcfd88 100644 --- a/apps/ensindexer/src/lib/tokenscope-helpers.ts +++ b/apps/ensindexer/src/lib/tokenscope-helpers.ts @@ -3,6 +3,7 @@ import { ChainId, DatasourceNames, ENSNamespaceId, + getChainIdsForNamespace, isChainAddressEqual, maybeGetDatasourceContractChainAddress, } from "@ensnode/datasources"; @@ -15,7 +16,7 @@ import { makeSubdomainNode, uint256ToHex32, } from "@ensnode/ensnode-sdk"; -import { Address, isAddressEqual } from "viem"; +import { Address, zeroAddress } from "viem"; import { base, baseSepolia, @@ -39,24 +40,6 @@ export type TokenType = (typeof TokenTypes)[keyof typeof TokenTypes]; */ export type TokenId = bigint; -/** - * Identifies a specific currency. - */ -export interface Currency { - symbol: string; - name: string; - decimals: number; - // For native currencies, address will be null - address: Address | null; -} - -/** - * Identifies a specific currency on a specific chain. - */ -export interface ChainCurrency extends Currency { - chainId: ChainId; -} - /** * A contract that issues tokenized ENS names. */ @@ -243,174 +226,182 @@ export function getDomainIdByTokenId( return knownTokenIssuingContract.getDomainId(tokenId); } -// Well-known currencies -const ETH_CURRENCY = { - symbol: "ETH", - name: "Ethereum", - decimals: 18, - address: null, +// TODO: Add support for WETH +/** + * Identifiers for supported currencies. + */ +export const CurrencyIds = { + ETH: "ETH", + USDC: "USDC", + DAI: "DAI", } as const; -const CHAIN_CURRENCIES = { - // Mainnet - [mainnet.id]: [ - { - symbol: "USDC", - name: "USD Coin", - decimals: 6, - address: "0xA0b86a33E6417c5Dd4Baf8C54e5de49E293E9169" as Address, - }, - { - symbol: "DAI", - name: "Dai Stablecoin", - decimals: 18, - address: "0x6B175474E89094C44Da98b954EedeAC495271d0F" as Address, - }, - ], - // Base - [base.id]: [ - { - symbol: "USDC", - name: "USD Coin", - decimals: 6, - address: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913" as Address, - }, - { - symbol: "DAI", - name: "Dai Stablecoin", - decimals: 18, - address: "0x50c5725949A6F0c72E6C4a641F24049A917DB0Cb" as Address, - }, - ], - // Optimism - [optimism.id]: [ - { - symbol: "USDC", - name: "USD Coin", - decimals: 6, - address: "0x0b2C639c533813f4Aa9D7837CAf62653d097Ff85" as Address, - }, - { - symbol: "DAI", - name: "Dai Stablecoin", - decimals: 18, - address: "0xDA10009cBd5D07dd0CeCc66161FC93D7c9000da1" as Address, - }, - ], - // Linea - [linea.id]: [ - { - symbol: "USDC", - name: "USD Coin", - decimals: 6, - address: "0x176211869cA2b568f2A7D4EE941E073a821EE1ff" as Address, - }, - { - symbol: "DAI", - name: "Dai Stablecoin", - decimals: 18, - address: "0x4AF15ec2A0BD43Db75dd04E62FAA3B8EF36b00d5" as Address, - }, - ], - // Sepolia - [sepolia.id]: [ - { - symbol: "USDC", - name: "USD Coin", - decimals: 6, - address: "0x1c7D4B196Cb0C7B01d743Fbc6116a902379C7238" as Address, - }, - { - symbol: "DAI", - name: "Dai Stablecoin", - decimals: 18, - address: "0x3e622317f8C93f7328350cF0B56d9eD4C620C5d6" as Address, - }, - ], - // Holesky - [holesky.id]: [ - { - symbol: "USDC", - name: "USD Coin", - decimals: 6, - address: "0x1c7D4B196Cb0C7B01d743Fbc6116a902379C7238" as Address, - }, - { - symbol: "DAI", - name: "Dai Stablecoin", - decimals: 18, - address: "0x3e622317f8C93f7328350cF0B56d9eD4C620C5d6" as Address, - }, - ], - // Base Sepolia - [baseSepolia.id]: [ - { - symbol: "USDC", - name: "USD Coin", - decimals: 6, - address: "0x036CbD53842c5426634e7929541eC2318f3dCF7e" as Address, - }, - { - symbol: "DAI", - name: "Dai Stablecoin", - decimals: 18, - address: "0x7368C6C68a4b2b68F90DB2e8F5E3b8E1E5e4F5c7" as Address, - }, - ], - // Linea Sepolia - [lineaSepolia.id]: [ - { - symbol: "USDC", - name: "USD Coin", - decimals: 6, - address: "0x176211869cA2b568f2A7D4EE941E073a821EE1ff" as Address, - }, - { - symbol: "DAI", - name: "Dai Stablecoin", - decimals: 18, - address: "0x4AF15ec2A0BD43Db75dd04E62FAA3B8EF36b00d5" as Address, - }, - ], +export type CurrencyId = (typeof CurrencyIds)[keyof typeof CurrencyIds]; + +export interface CurrencyConfig { + id: CurrencyId; + name: string; + decimals: number; +} + +const currencyConfigs: Record = { + [CurrencyIds.ETH]: { + id: CurrencyIds.ETH, + name: "Ethereum", + decimals: 18, + }, + [CurrencyIds.USDC]: { + id: CurrencyIds.USDC, + name: "USD Coin", + decimals: 6, + }, + [CurrencyIds.DAI]: { + id: CurrencyIds.DAI, + name: "Dai Stablecoin", + decimals: 18, + }, +} as const; + +export const getCurrencyConfig = (currencyId: CurrencyId): CurrencyConfig => { + return currencyConfigs[currencyId]; +}; + +// NOTE: this mapping currently only considers the subset of chains where we have +// supported token issuing contracts. +const knownCurrencyContracts: Record> = { + /** mainnet namespace */ + [mainnet.id]: { + [CurrencyIds.ETH]: zeroAddress, + [CurrencyIds.USDC]: "0xA0b86a33E6417c5Dd4Baf8C54e5de49E293E9169", + [CurrencyIds.DAI]: "0x6B175474E89094C44Da98b954EedeAC495271d0F", + }, + [base.id]: { + [CurrencyIds.ETH]: zeroAddress, + [CurrencyIds.USDC]: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", + [CurrencyIds.DAI]: "0x50c5725949A6F0c72E6C4a641F24049A917DB0Cb", + }, + [optimism.id]: { + [CurrencyIds.ETH]: zeroAddress, + [CurrencyIds.USDC]: "0x0b2C639c533813f4Aa9D7837CAf62653d097Ff85", + [CurrencyIds.DAI]: "0xDA10009cBd5D07dd0CeCc66161FC93D7c9000da1", + }, + [linea.id]: { + [CurrencyIds.ETH]: zeroAddress, + [CurrencyIds.USDC]: "0x176211869cA2b568f2A7D4EE941E073a821EE1ff", + [CurrencyIds.DAI]: "0x4AF15ec2A0BD43Db75dd04E62FAA3B8EF36b00d5", + }, + /** sepolia namespace */ + [sepolia.id]: { + [CurrencyIds.ETH]: zeroAddress, + [CurrencyIds.USDC]: "0x1c7D4B196Cb0C7B01d743Fbc6116a902379C7238", + [CurrencyIds.DAI]: "0x3e622317f8C93f7328350cF0B56d9eD4C620C5d6", + }, + [baseSepolia.id]: { + [CurrencyIds.ETH]: zeroAddress, + [CurrencyIds.USDC]: "0x036CbD53842c5426634e7929541eC2318f3dCF7e", + [CurrencyIds.DAI]: "0x7368C6C68a4b2b68F90DB2e8F5E3b8E1E5e4F5c7", + }, + [lineaSepolia.id]: { + [CurrencyIds.ETH]: zeroAddress, + [CurrencyIds.USDC]: "0x176211869cA2b568f2A7D4EE941E073a821EE1ff", + [CurrencyIds.DAI]: "0x4AF15ec2A0BD43Db75dd04E62FAA3B8EF36b00d5", + }, + [holesky.id]: { + [CurrencyIds.ETH]: zeroAddress, + [CurrencyIds.USDC]: "0x1c7D4B196Cb0C7B01d743Fbc6116a902379C7238", + [CurrencyIds.DAI]: "0x3e622317f8C93f7328350cF0B56d9eD4C620C5d6", + }, } as const; /** - * Returns an array of supported currencies for a given chain ID. + * Gets the supported currency contracts for a given chain. * - * @param chainId - The chain ID to get supported currencies for - * @returns an array of ChainCurrency objects representing supported currencies on the chain + * @param chainId - The chain ID to get supported currency contracts for + * @returns a record of currency ids to ChainAddresses for the given chain */ -export const getSupportedCurrencies = (chainId: ChainId): ChainCurrency[] => { - const chainCurrencies = CHAIN_CURRENCIES[chainId as keyof typeof CHAIN_CURRENCIES] || []; +export const getSupportedCurrencyContractsForChain = ( + chainId: ChainId, +): Record => { + let result = {} as Record; - // Always add ETH as the native currency - const currencies: ChainCurrency[] = [ - { - ...ETH_CURRENCY, - chainId, - }, - ]; + const knownCurrencyContractsForChain = knownCurrencyContracts[chainId]; + if (!knownCurrencyContractsForChain) { + return result; + } - // Add chain-specific currencies - currencies.push( - ...chainCurrencies.map((currency) => ({ - ...currency, + for (const [currencyId, address] of Object.entries(knownCurrencyContractsForChain)) { + result[currencyId as CurrencyId] = { + address, chainId, - })), - ); + } as ChainAddress; + } - return currencies; + return result; +}; + +/** + * Gets the supported currency contracts for a given namespace. + * + * @param namespaceId - The ENSNamespace identifier (e.g. 'mainnet', 'sepolia', 'holesky', + * 'ens-test-env') + * @returns a record of currency ids to ChainAddresses for the given namespace + */ +export const getSupportedCurrencyContractsForNamespace = ( + namespaceId: ENSNamespaceId, +): Record => { + let result = {} as Record; + const chainIds = getChainIdsForNamespace(namespaceId); + for (const chainId of chainIds) { + const supportedCurrencyContractsForChain = getSupportedCurrencyContractsForChain(chainId); + result = { ...result, ...supportedCurrencyContractsForChain }; + } + + return result; }; /** - * Identifies if the provided ChainAddress is a supported currency contract. + * Identifies if the provided ChainAddress is a supported currency contract in the + * specified namespace. * + * @param namespaceId - The ENSNamespace identifier (e.g. 'mainnet', 'sepolia', 'holesky', + * 'ens-test-env') * @param contract - The ChainAddress of the contract to check - * @returns a boolean indicating if the provided ChainAddress is a supported currency contract + * @returns a boolean indicating if the provided ChainAddress is a supported currency + * contract in the specified namespace */ -export const isSupportedCurrencyContract = (contract: ChainAddress): boolean => { - const supportedCurrencies = getSupportedCurrencies(contract.chainId); - return supportedCurrencies.some( - (currency) => currency.address && isAddressEqual(currency.address, contract.address), +export const isSupportedCurrencyContract = ( + namespaceId: ENSNamespaceId, + contract: ChainAddress, +): boolean => { + const supportedCurrencyContracts = getSupportedCurrencyContractsForNamespace(namespaceId); + return Object.values(supportedCurrencyContracts).some((supportedCurrencyContract) => + isChainAddressEqual(supportedCurrencyContract, contract), ); }; + +/** + * Gets the currency id for the given contract in the specified namespace. + * + * @param namespaceId - The ENSNamespace identifier (e.g. 'mainnet', 'sepolia', 'holesky', + * 'ens-test-env') + * @param contract - The ChainAddress of the contract to get the currency id for + * @returns the currency id for the given contract in the specified namespace, or + * null if the contract is not a supported currency contract in the + * specified namespace + */ +export const getCurrencyIdForContract = ( + namespaceId: ENSNamespaceId, + contract: ChainAddress, +): CurrencyId | null => { + const supportedCurrencyContracts = getSupportedCurrencyContractsForNamespace(namespaceId); + + for (const [currencyId, supportedCurrencyContract] of Object.entries( + supportedCurrencyContracts, + )) { + if (isChainAddressEqual(supportedCurrencyContract, contract)) { + return currencyId as CurrencyId; + } + } + + return null; +}; diff --git a/packages/datasources/src/namespaces.ts b/packages/datasources/src/namespaces.ts index 004ee327a..33f895cc1 100644 --- a/packages/datasources/src/namespaces.ts +++ b/packages/datasources/src/namespaces.ts @@ -2,6 +2,7 @@ import ensTestEnv from "./ens-test-env"; import holesky from "./holesky"; import { ChainAddress, + ChainId, Datasource, DatasourceName, DatasourceNames, @@ -114,3 +115,16 @@ export const getENSRootChain = (namespaceId: ENSNamespaceId) => * @returns the chain ID that hosts the ENS Root */ export const getENSRootChainId = (namespaceId: ENSNamespaceId) => getENSRootChain(namespaceId).id; + +/** + * Gets all the chainIds with datasources in the specified namespace. + * + * @param namespaceId - The ENSNamespace identifier (e.g. 'mainnet', 'sepolia', 'holesky', + * 'ens-test-env') + * @returns an array of distinct chainIds with datasources in the specified namespace + */ +export const getChainIdsForNamespace = (namespaceId: ENSNamespaceId): ChainId[] => { + const namespace = getENSNamespace(namespaceId); + const chainIds = Object.values(namespace).map((datasource) => datasource.chain.id); + return [...new Set(chainIds)]; +}; diff --git a/packages/ensnode-schema/src/tokenscope.schema.ts b/packages/ensnode-schema/src/tokenscope.schema.ts index 1e193e7d6..c9655e7ca 100644 --- a/packages/ensnode-schema/src/tokenscope.schema.ts +++ b/packages/ensnode-schema/src/tokenscope.schema.ts @@ -53,12 +53,11 @@ export const nameSold = onchainTable( newOwnerId: t.hex().notNull(), /** - * Currency address of the payment (ETH, USDC, WETH, or DAI). + * Currency id of the payment (ETH, USDC or DAI). * - * Works in conjunction with 'price' field to define the complete payment amount. - * Currency contract address varies by chainId - same token has different addresses on different chains. + * Works in conjunction with the 'price' field to define the payment. */ - currency: t.text().notNull(), + currencyId: t.text().notNull(), /** * The payment amount denominated in the smallest unit of the currency specified in 'currency' field. From 9c9032026604af410a5c6df5214fc7958a34e103 Mon Sep 17 00:00:00 2001 From: "lightwalker.eth" <126201998+lightwalker-eth@users.noreply.github.com> Date: Mon, 25 Aug 2025 15:28:44 +0300 Subject: [PATCH 14/61] Refine indexing logic --- apps/ensindexer/src/handlers/Seaport.ts | 362 ++++++++++++------ apps/ensindexer/src/lib/tokenscope-helpers.ts | 77 ++-- 2 files changed, 299 insertions(+), 140 deletions(-) diff --git a/apps/ensindexer/src/handlers/Seaport.ts b/apps/ensindexer/src/handlers/Seaport.ts index 7cf00b1ad..864a710bd 100644 --- a/apps/ensindexer/src/handlers/Seaport.ts +++ b/apps/ensindexer/src/handlers/Seaport.ts @@ -6,16 +6,23 @@ import config from "@/config"; import { sharedEventValues, upsertAccount } from "@/lib/db-helpers"; import { EventWithArgs } from "@/lib/ponder-helpers"; import { + CurrencyIds, + Price, + TokenId, + TokenType, TokenTypes, getCurrencyIdForContract, getDomainIdByTokenId, - isKnownTokenIssuingContract, + getKnownTokenIssuer, + isKnownTokenIssuer, } from "@/lib/tokenscope-helpers"; -import { ChainAddress, ChainId } from "@ensnode/datasources"; +import { ChainAddress, ChainId, ENSNamespaceId } from "@ensnode/datasources"; import { NameSoldInsert } from "@ensnode/ensnode-schema"; -import { Address, Hex, zeroAddress } from "viem"; +import type { Node } from "@ensnode/ensnode-sdk"; +import { Address, Hex } from "viem"; +import { PrivateKeyToAccountErrorType } from "viem/accounts"; -type OfferItem = { +type SeaportOfferItem = { /** * The type of item in the offer. * For example, ERC20, ERC721, ERC1155, or NATIVE (ETH) @@ -48,7 +55,7 @@ type OfferItem = { amount: bigint; }; -type ConsiderationItem = { +type SeaportConsiderationItem = { /** * The type of item in the consideration. * For example, ERC20, ERC721, ERC1155, or NATIVE (ETH) @@ -90,7 +97,7 @@ type ConsiderationItem = { interface SeaportOrderFulfilledEvent extends EventWithArgs<{ /** - * The unique hash identifier of the fulfilled order. + * The unique hash identifier of the fulfilled order within Seaport. * Used to track and reference specific orders on-chain. */ orderHash: Hex; @@ -120,161 +127,284 @@ interface SeaportOrderFulfilledEvent * For listings: NFTs/tokens being sold * For offers: ETH/ERC20 tokens being offered as payment */ - offer: readonly OfferItem[]; + offer: readonly SeaportOfferItem[]; /** * Array of items that the offerer expects to receive in return. * For listings: ETH/ERC20 tokens expected as payment * For offers: NFTs/tokens being requested in exchange */ - consideration: readonly ConsiderationItem[]; + consideration: readonly SeaportConsiderationItem[]; }> {} -/** - * Checks if an item is a supported NFT (ERC721/ERC1155 from known contracts) - */ -function isSupportedNFT(chainId: ChainId, item: OfferItem | ConsiderationItem): boolean { - const isValidItemType = item.itemType === ItemType.ERC721 || item.itemType === ItemType.ERC1155; - const isSupportedContract = isKnownTokenIssuingContract(config.namespace, { - chainId, - address: item.token, - }); +interface SupportedNFT { + tokenType: TokenType; + contract: ChainAddress; + tokenId: TokenId; + domainId: Node; +} - return isValidItemType && isSupportedContract; +interface SupportedPayment { + price: Price; } /** - * Checks if an item is a payment token (ETH or ERC20) + * Gets the supported TokenScope token type for a given Seaport item type. + * + * @param itemType - The Seaport item type to get the supported TokenScope token type for + * @returns the supported TokenScope token type for the given SeaPort item type, or null + * if the item type is not supported */ -function isPaymentToken(item: OfferItem | ConsiderationItem): boolean { - return item.itemType === ItemType.NATIVE || item.itemType === ItemType.ERC20; -} +const getSupportedTokenType = (itemType: ItemType): TokenType | null => { + if (itemType === ItemType.ERC721) { + return TokenTypes.ERC721; + } else if (itemType === ItemType.ERC1155) { + return TokenTypes.ERC1155; + } else { + return null; + } +}; /** - * Determines if a Seaport order fulfillment meets our criteria for indexing. + * Gets the supported NFT from a given Seaport item. * - * @returns indexable sale data, or `null` if the order fulfilled event does - * not meet our criteria for indexing. + * @param namespaceId - The ENSNamespace identifier (e.g. 'mainnet', 'sepolia', 'holesky', + * 'ens-test-env') + * @param chainId - The chain ID of the Seaport item + * @param item - The Seaport item to get the supported NFT from + * @returns the supported NFT from the given Seaport item, or `null` if the Seaport item is + * not a supported NFT */ -function getIndexableSale( - event: SeaportOrderFulfilledEvent, +const getSupportedNFT = ( + namespaceId: ENSNamespaceId, chainId: ChainId, -): NameSoldInsert | null { - const { offer, consideration, orderHash, offerer, recipient } = event.args; - - // Find all NFTs and payment items - const nftsInOffer = offer.filter((item) => isSupportedNFT(chainId, item)); - const nftsInConsideration = consideration.filter((item) => isSupportedNFT(chainId, item)); - const paymentsInOffer = offer.filter(isPaymentToken); - const paymentsInConsideration = consideration.filter(isPaymentToken); - - let nftItem: OfferItem | ConsiderationItem; - let paymentItems: (OfferItem | ConsiderationItem)[]; - let seller: Address; - let buyer: Address; - - // Determine transaction type and validate structure - if ( - nftsInOffer.length === 1 && - nftsInConsideration.length === 0 && - paymentsInConsideration.length > 0 - ) { - // Listing: NFT in offer, payment in consideration - nftItem = nftsInOffer[0]!; - paymentItems = paymentsInConsideration; - seller = offerer; - buyer = recipient; - } else if ( - nftsInConsideration.length === 1 && - nftsInOffer.length === 0 && - paymentsInOffer.length > 0 - ) { - // Offer: payment in offer, NFT in consideration - nftItem = nftsInConsideration[0]!; - paymentItems = paymentsInOffer; - seller = recipient; - buyer = offerer; - } else { - // Invalid structure + item: SeaportOfferItem | SeaportConsiderationItem, +): SupportedNFT | null => { + // validate item as an ERC721/ERC1155 NFT + const tokenType = getSupportedTokenType(item.itemType); + if (!tokenType) { return null; } - // Validate payment structure - if (paymentItems.length === 0) { + // validate that the token is a known token issuing contract + const tokenIssuer = getKnownTokenIssuer(namespaceId, { + chainId, + address: item.token, + }); + if (!tokenIssuer) { return null; } - // Check for mixed currencies - const paymentTokens = paymentItems.map((item) => item.token.toLowerCase()); - const uniqueTokens = [...new Set(paymentTokens)]; - if (uniqueTokens.length > 1) { - return null; // Mixed currencies not supported - } + const contract = tokenIssuer.contract; + const tokenId = item.identifier; + const domainId = tokenIssuer.getDomainId(tokenId); - const currencyContract: ChainAddress = { + return { + tokenType, + contract, + tokenId, + domainId, + } satisfies SupportedNFT; +}; + +const getSupportedPayment = ( + namespaceId: ENSNamespaceId, + chainId: ChainId, + item: SeaportOfferItem | SeaportConsiderationItem, +): SupportedPayment | null => { + const currencyContract = { chainId, - address: paymentItems[0]!.token, + address: item.token, }; - const currencyId = getCurrencyIdForContract(config.namespace, currencyContract); + // validate that the item is a supported currency + const currencyId = getCurrencyIdForContract(namespaceId, currencyContract); if (!currencyId) { return null; // Unsupported currency } - // Calculate total payment amount - const totalAmount = paymentItems.reduce((total, item) => total + item.amount, 0n); - if (totalAmount <= 0n) { + // validate the Seaport item type is supported and matches the currencyId + if (item.itemType === ItemType.NATIVE) { + if (currencyId !== CurrencyIds.ETH) { + return null; // Seaport item type doesn't match currencyId + } + } else if (item.itemType === ItemType.ERC20) { + if (currencyId === CurrencyIds.ETH) { + return null; // Seaport item type doesn't match currencyId + } + } else { + // unsupported Seaport item type return null; } - // Extract NFT details - const tokenId = nftItem.identifier.toString(); + if (item.amount < 0n) { + return null; // Invalid amount + } - const nftContract: ChainAddress = { - chainId, - address: nftItem.token, - }; + return { + price: { + currency: currencyId, + amount: item.amount, + }, + } satisfies SupportedPayment; +}; - // Get domain ID - let domainId; - try { - domainId = getDomainIdByTokenId(config.namespace, nftContract, nftItem.identifier); - } catch { - // unsupported NFT contract - return null; +interface SeaportItemExtractions { + nfts: SupportedNFT[]; + payments: SupportedPayment[]; +} + +const getSeaportItemExtractions = ( + namespaceId: ENSNamespaceId, + chainId: ChainId, + items: readonly (SeaportOfferItem | SeaportConsiderationItem)[], +): SeaportItemExtractions => { + let nfts: SupportedNFT[] = []; + let payments: SupportedPayment[] = []; + + // each item is either a supported NFT, a supported payment, or unsupported + for (const item of items) { + const nft = getSupportedNFT(namespaceId, chainId, item); + if (nft) { + nfts.push(nft); + } else { + const payment = getSupportedPayment(namespaceId, chainId, item); + if (payment) { + payments.push(payment); + } + } } return { - ...sharedEventValues(chainId, event), - logIndex: event.log.logIndex, - chainId, - orderHash, - timestamp: event.block.timestamp, - fromOwnerId: seller, - newOwnerId: buyer, - contractAddress: nftContract.address, - tokenId, - tokenType: nftItem.itemType === ItemType.ERC721 ? TokenTypes.ERC721 : TokenTypes.ERC1155, - domainId, - currencyId, - price: totalAmount, - }; + nfts, + payments, + } satisfies SeaportItemExtractions; +}; + +const consolidateSupportedNFTs = (nfts: SupportedNFT[]): SupportedNFT | null => { + if (nfts.length !== 1) { + return null; // Either no NFT or multiple NFTs + } + + return nfts[0]!; +}; + +const consolidateSupportedPayments = (payments: SupportedPayment[]): SupportedPayment | null => { + // Get the set of distinct currencies in the payment + const paymentCurrencies = payments.map((payment) => payment.price.currency); + const uniqueCurrencies = [...new Set(paymentCurrencies)]; + if (uniqueCurrencies.length !== 1) { + return null; // Either no payment or multiple payments in mixed currencies + } + + const totalAmount = payments.reduce((total, payment) => total + payment.price.amount, 0n); + + return { + price: { + currency: uniqueCurrencies[0]!, // we verified above there's exactly one currency + amount: totalAmount, + }, + } satisfies SupportedPayment; +}; + +interface SupportedSale { + nft: SupportedNFT; + payment: SupportedPayment; + seller: Address; + buyer: Address; } +const getSupportedSale = ( + namespaceId: ENSNamespaceId, + chainId: ChainId, + event: SeaportOrderFulfilledEvent, +): SupportedSale | null => { + const { offer, consideration, orderHash, offerer, recipient } = event.args; + + const { nfts: offerNFTs, payments: offerPayments } = getSeaportItemExtractions( + namespaceId, + chainId, + offer, + ); + const { nfts: considerationNFTs, payments: considerationPayments } = getSeaportItemExtractions( + namespaceId, + chainId, + consideration, + ); + + const consolidatedOfferNFT = consolidateSupportedNFTs(offerNFTs); + const consolidatedConsiderationNFT = consolidateSupportedNFTs(considerationNFTs); + const consolidatedOfferPayment = consolidateSupportedPayments(offerPayments); + const consolidatedConsiderationPayment = consolidateSupportedPayments(considerationPayments); + + if ( + consolidatedOfferNFT && + !consolidatedConsiderationNFT && + consolidatedOfferPayment && + !consolidatedConsiderationPayment + ) { + // offer is exactly 1 supported NFT and consideration consolidates to 1 supported payment + // therefore the offerer is the seller and the recipient is the buyer + return { + nft: consolidatedOfferNFT, + payment: consolidatedOfferPayment, + seller: offerer, + buyer: recipient, + } satisfies SupportedSale; + } else if ( + !consolidatedOfferNFT && + consolidatedConsiderationNFT && + !consolidatedOfferPayment && + consolidatedConsiderationPayment + ) { + // consideration is exactly 1 supported NFT and offer consolidates to 1 supported payment + // therefore the recipient is the seller and the offerer is the buyer + return { + nft: consolidatedConsiderationNFT, + payment: consolidatedConsiderationPayment, + seller: recipient, + buyer: offerer, + } satisfies SupportedSale; + } else { + // unsupported sale + return null; + } +}; + /** - * Processes a validated sale transaction + * Indexes a supported sale transaction */ -async function handleSale(context: Context, saleData: NameSoldInsert): Promise { - // Ensure accounts exist - await upsertAccount(context, saleData.fromOwnerId); - await upsertAccount(context, saleData.newOwnerId); +const indexSupportedSale = async ( + context: Context, + event: SeaportOrderFulfilledEvent, + sale: SupportedSale, +): Promise => { + // Ensure buyer and selleraccounts exist + await upsertAccount(context, sale.seller); + await upsertAccount(context, sale.buyer); + + const saleData: NameSoldInsert = { + ...sharedEventValues(context.chain.id, event), + logIndex: event.log.logIndex, + chainId: context.chain.id, + orderHash: event.args.orderHash, + timestamp: event.block.timestamp, + fromOwnerId: sale.seller, + newOwnerId: sale.buyer, + contractAddress: sale.nft.contract.address, + tokenId: sale.nft.tokenId.toString(), // TODO: store as a bigint? + tokenType: sale.nft.tokenType, + domainId: sale.nft.domainId, + currencyId: sale.payment.price.currency, + price: sale.payment.price.amount, + }; - // Record the sale + // Index the sale await context.db.insert(schema.nameSold).values(saleData); -} +}; /** - * Main handler for Seaport OrderFulfilled events + * Handles each Seaport OrderFulfilled event */ export async function handleOrderFulfilled({ context, @@ -283,8 +413,8 @@ export async function handleOrderFulfilled({ context: Context; event: SeaportOrderFulfilledEvent; }) { - const indexableSale = getIndexableSale(event, context.chain.id); - if (indexableSale) { - await handleSale(context, indexableSale); + const supportedSale = getSupportedSale(config.namespace, context.chain.id, event); + if (supportedSale) { + await indexSupportedSale(context, event, supportedSale); } } diff --git a/apps/ensindexer/src/lib/tokenscope-helpers.ts b/apps/ensindexer/src/lib/tokenscope-helpers.ts index 46dbcfd88..ea1c410de 100644 --- a/apps/ensindexer/src/lib/tokenscope-helpers.ts +++ b/apps/ensindexer/src/lib/tokenscope-helpers.ts @@ -43,15 +43,15 @@ export type TokenId = bigint; /** * A contract that issues tokenized ENS names. */ -export interface TokenIssuingContract { +export interface TokenIssuer { /** - * The ChainAddress of the token issuing contract. + * The ChainAddress of the token issuer contract. */ contract: ChainAddress; /** - * Applies the contract's logic for converting from the token id - * representation of a name to the domain id (Node) of the name. + * Applies the token issuer contract's logic for converting from the token id + * representation of a domain to the domain id (Node) representation of a domain. */ getDomainId: (tokenId: TokenId) => Node; } @@ -87,11 +87,9 @@ export const labelHashGeneratedTokenIdToNode = (tokenId: TokenId, parentNode: No * specified namespace. * * @param namespaceId - The ENSNamespace identifier (e.g. 'mainnet', 'sepolia', 'holesky', 'ens-test-env') - * @returns an array of 0 or more known TokenIssuingContract for the specified namespace + * @returns an array of 0 or more known TokenIssuer for the specified namespace */ -export const getKnownTokenIssuingContracts = ( - namespaceId: ENSNamespaceId, -): TokenIssuingContract[] => { +export const getKnownTokenIssuers = (namespaceId: ENSNamespaceId): TokenIssuer[] => { const ethBaseRegistrar = maybeGetDatasourceContractChainAddress( namespaceId, DatasourceNames.ENSRoot, @@ -123,7 +121,7 @@ export const getKnownTokenIssuingContracts = ( "BaseRegistrar", ); - const result: TokenIssuingContract[] = []; + const result: TokenIssuer[] = []; if (ethBaseRegistrar) { result.push({ @@ -183,26 +181,45 @@ export const getKnownTokenIssuingContracts = ( }; /** - * Identifies if the provided ChainAddress is a known token issuing contract. + * Gets the known token issuer for the given ChainAddress in the specified namespace. * - * @param namespaceId - The ENSNamespace identifier (e.g. 'mainnet', 'sepolia', 'holesky', 'ens-test-env') + * @param namespaceId - The ENSNamespace identifier (e.g. 'mainnet', 'sepolia', 'holesky', + * 'ens-test-env') + * @param contract - The ChainAddress to get the known token issuer for + * @returns the known token issuer for the given ChainAddress, or null + * if the ChainAddress is not a known token issuer in the specified namespace + */ +export const getKnownTokenIssuer = ( + namespaceId: ENSNamespaceId, + contract: ChainAddress, +): TokenIssuer | null => { + const tokenIssuers = getKnownTokenIssuers(namespaceId); + return ( + tokenIssuers.find((tokenIssuer) => isChainAddressEqual(tokenIssuer.contract, contract)) ?? null + ); +}; + +/** + * Identifies if the provided ChainAddress is a known token issuer. + * + * @param namespaceId - The ENSNamespace identifier (e.g. 'mainnet', 'sepolia', 'holesky', + * 'ens-test-env') * @param chainAddress - The ChainAddress to check - * @returns a boolean indicating if the provided ChainAddress is a known token issuing contract + * @returns a boolean indicating if the provided ChainAddress is a known token issuer in + * the specified namespace. */ -export const isKnownTokenIssuingContract = ( +export const isKnownTokenIssuer = ( namespaceId: ENSNamespaceId, chainAddress: ChainAddress, ): boolean => { - const knownTokenIssuingContracts = getKnownTokenIssuingContracts(namespaceId); - return knownTokenIssuingContracts.some((tokenIssuingContract) => - isChainAddressEqual(tokenIssuingContract.contract, chainAddress), - ); + return getKnownTokenIssuer(namespaceId, chainAddress) !== null; }; /** * Gets the domainId (Node) for a given NFT from contract with tokenId on the specified namespace. * - * @param namespaceId - The ENSNamespace identifier (e.g. 'mainnet', 'sepolia', 'holesky', 'ens-test-env') + * @param namespaceId - The ENSNamespace identifier (e.g. 'mainnet', 'sepolia', 'holesky', + * 'ens-test-env') * @param contract - The ChainAddress of the NFT contract * @param tokenId - The tokenId of the NFT * @returns the domainId (Node) for ENS name associated with the NFT @@ -213,17 +230,17 @@ export function getDomainIdByTokenId( contract: ChainAddress, tokenId: TokenId, ): Node { - const knownTokenIssuingContracts = getKnownTokenIssuingContracts(namespaceId); - const knownTokenIssuingContract = knownTokenIssuingContracts.find((tokenIssuingContract) => - isChainAddressEqual(tokenIssuingContract.contract, contract), + const tokenIssuers = getKnownTokenIssuers(namespaceId); + const tokenIssuer = tokenIssuers.find((tokenIssuer) => + isChainAddressEqual(tokenIssuer.contract, contract), ); - if (!knownTokenIssuingContract) { + if (!tokenIssuer) { throw new Error( - `The contract at address ${contract.address} on chain ${contract.chainId} is not a known token issuing contract in the ${namespaceId} namespace`, + `The contract at address ${contract.address} on chain ${contract.chainId} is not a known token issuer in the ${namespaceId} namespace`, ); } - return knownTokenIssuingContract.getDomainId(tokenId); + return tokenIssuer.getDomainId(tokenId); } // TODO: Add support for WETH @@ -238,6 +255,18 @@ export const CurrencyIds = { export type CurrencyId = (typeof CurrencyIds)[keyof typeof CurrencyIds]; +export interface Price { + currency: CurrencyId; + + /** + * The amount of the currency in the smallest unit of the currency. (see + * decimals of the CurrencyConfig for the currency). + * + * Guaranteed to be non-negative. + */ + amount: bigint; +} + export interface CurrencyConfig { id: CurrencyId; name: string; From 3d12ddec4410c406ea0e613eeb4cc5131773a229 Mon Sep 17 00:00:00 2001 From: "lightwalker.eth" <126201998+lightwalker-eth@users.noreply.github.com> Date: Mon, 25 Aug 2025 15:31:38 +0300 Subject: [PATCH 15/61] Remove unnecessary .gitattributes --- .gitattributes | 2 -- 1 file changed, 2 deletions(-) delete mode 100644 .gitattributes diff --git a/.gitattributes b/.gitattributes deleted file mode 100644 index eba1110b5..000000000 --- a/.gitattributes +++ /dev/null @@ -1,2 +0,0 @@ -# Auto detect text files and perform LF normalization -* text=auto \ No newline at end of file From 6507c95e89e83fd550be8c00e733f7989b916fdb Mon Sep 17 00:00:00 2001 From: "lightwalker.eth" <126201998+lightwalker-eth@users.noreply.github.com> Date: Mon, 25 Aug 2025 15:51:47 +0300 Subject: [PATCH 16/61] Make indexing of Seaport1.5 explicit --- apps/ensindexer/src/plugins/tokenscope/plugin.ts | 4 ++-- .../src/abis/seaport/{Seaport.ts => Seaport1.5.ts} | 0 packages/datasources/src/mainnet.ts | 6 +++--- packages/datasources/src/sepolia.ts | 6 +++--- 4 files changed, 8 insertions(+), 8 deletions(-) rename packages/datasources/src/abis/seaport/{Seaport.ts => Seaport1.5.ts} (100%) diff --git a/apps/ensindexer/src/plugins/tokenscope/plugin.ts b/apps/ensindexer/src/plugins/tokenscope/plugin.ts index 752b710ae..84dace2a1 100644 --- a/apps/ensindexer/src/plugins/tokenscope/plugin.ts +++ b/apps/ensindexer/src/plugins/tokenscope/plugin.ts @@ -33,10 +33,10 @@ export default createPlugin({ ...chainConfigForContract( config.globalBlockrange, seaport.chain.id, - seaport.contracts.Seaport, + seaport.contracts.Seaport1_5, ), }, - abi: seaport.contracts.Seaport.abi, + abi: seaport.contracts.Seaport1_5.abi, }, }, }); diff --git a/packages/datasources/src/abis/seaport/Seaport.ts b/packages/datasources/src/abis/seaport/Seaport1.5.ts similarity index 100% rename from packages/datasources/src/abis/seaport/Seaport.ts rename to packages/datasources/src/abis/seaport/Seaport1.5.ts diff --git a/packages/datasources/src/mainnet.ts b/packages/datasources/src/mainnet.ts index fe60d7088..8e165d39a 100644 --- a/packages/datasources/src/mainnet.ts +++ b/packages/datasources/src/mainnet.ts @@ -25,7 +25,7 @@ import { NameWrapper as linea_NameWrapper } from "./abis/lineanames/NameWrapper" import { Registry as linea_Registry } from "./abis/lineanames/Registry"; import { ThreeDNSToken } from "./abis/threedns/ThreeDNSToken"; -import { Seaport } from "./abis/seaport/Seaport"; +import { Seaport as Seaport1_5 } from "./abis/seaport/Seaport1.5"; // Shared ABIs import { StandaloneReverseRegistrar } from "./abis/shared/StandaloneReverseRegistrar"; import { ResolverABI, ResolverFilter } from "./lib/resolver"; @@ -431,8 +431,8 @@ export default { [DatasourceNames.Seaport]: { chain: mainnet, contracts: { - Seaport: { - abi: Seaport, + Seaport1_5: { + abi: Seaport1_5, // Seaport 1.5 address: "0x00000000000000ADc04C56Bf30aC9d3c0aAF14dC", startBlock: 17129405, }, diff --git a/packages/datasources/src/sepolia.ts b/packages/datasources/src/sepolia.ts index 41dfa1577..d2030885f 100644 --- a/packages/datasources/src/sepolia.ts +++ b/packages/datasources/src/sepolia.ts @@ -30,7 +30,7 @@ import { EthRegistrarController as linea_EthRegistrarController } from "./abis/l import { NameWrapper as linea_NameWrapper } from "./abis/lineanames/NameWrapper"; import { Registry as linea_Registry } from "./abis/lineanames/Registry"; -import { Seaport } from "./abis/seaport/Seaport"; +import { Seaport as Seaport1_5 } from "./abis/seaport/Seaport1.5"; // Shared ABIs import { StandaloneReverseRegistrar } from "./abis/shared/StandaloneReverseRegistrar"; import { ResolverABI, ResolverFilter } from "./lib/resolver"; @@ -356,8 +356,8 @@ export default { [DatasourceNames.Seaport]: { chain: sepolia, contracts: { - Seaport: { - abi: Seaport, + Seaport1_5: { + abi: Seaport1_5, // Seaport 1.5 address: "0x00000000000000ADc04C56Bf30aC9d3c0aAF14dC", startBlock: 3365529, }, From bcc38ae83a1f3d41b3e253c8a19bd61155e5de4f Mon Sep 17 00:00:00 2001 From: "lightwalker.eth" <126201998+lightwalker-eth@users.noreply.github.com> Date: Mon, 25 Aug 2025 15:53:29 +0300 Subject: [PATCH 17/61] Remove circular package dependency --- packages/datasources/package.json | 1 - pnpm-lock.yaml | 39 ------------------------------- 2 files changed, 40 deletions(-) diff --git a/packages/datasources/package.json b/packages/datasources/package.json index 49edc0276..fd70edca8 100644 --- a/packages/datasources/package.json +++ b/packages/datasources/package.json @@ -47,7 +47,6 @@ "viem": "catalog:" }, "dependencies": { - "@ensnode/ensnode-sdk": "workspace:*", "@ponder/utils": "catalog:" } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 35da1405a..144ce1143 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -6,45 +6,15 @@ settings: catalogs: default: - '@astrojs/react': - specifier: ^4.2.0 - version: 4.2.0 - '@astrojs/tailwind': - specifier: ^6.0.0 - version: 6.0.0 '@biomejs/biome': specifier: ^1.9.4 version: 1.9.4 - '@namehash/namekit-react': - specifier: 0.12.0 - version: 0.12.0 '@ponder/utils': specifier: 0.2.10 version: 0.2.10 '@types/node': specifier: ^22.14.0 version: 22.15.3 - '@vitest/coverage-v8': - specifier: ^3.1.1 - version: 3.1.1 - astro-font: - specifier: ^1.0.0 - version: 1.0.0 - astro-seo: - specifier: ^0.8.4 - version: 0.8.4 - date-fns: - specifier: ^4.1.0 - version: 4.1.0 - drizzle-orm: - specifier: '=0.41.0' - version: 0.41.0 - hono: - specifier: ^4.7.6 - version: 4.7.8 - ponder: - specifier: 0.11.43 - version: 0.11.43 tsup: specifier: ^8.3.6 version: 8.3.6 @@ -54,12 +24,6 @@ catalogs: viem: specifier: ^2.22.13 version: 2.23.2 - vitest: - specifier: ^3.1.1 - version: 3.1.1 - zod: - specifier: ^3.25.7 - version: 3.25.7 overrides: '@graphiql/react>@headlessui/react': 2.2.0 @@ -572,9 +536,6 @@ importers: packages/datasources: dependencies: - '@ensnode/ensnode-sdk': - specifier: workspace:* - version: link:../ensnode-sdk '@ponder/utils': specifier: 'catalog:' version: 0.2.10(typescript@5.7.3)(viem@2.23.2(bufferutil@4.0.9)(typescript@5.7.3)(utf-8-validate@5.0.10)(zod@3.25.7)) From f8e50dcc93df1969e924a4a4a668997b7eaa1072 Mon Sep 17 00:00:00 2001 From: "lightwalker.eth" <126201998+lightwalker-eth@users.noreply.github.com> Date: Mon, 25 Aug 2025 16:02:13 +0300 Subject: [PATCH 18/61] Refine docs --- packages/datasources/src/namespaces.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/datasources/src/namespaces.ts b/packages/datasources/src/namespaces.ts index 33f895cc1..b4ebd18d4 100644 --- a/packages/datasources/src/namespaces.ts +++ b/packages/datasources/src/namespaces.ts @@ -117,7 +117,8 @@ export const getENSRootChain = (namespaceId: ENSNamespaceId) => export const getENSRootChainId = (namespaceId: ENSNamespaceId) => getENSRootChain(namespaceId).id; /** - * Gets all the chainIds with datasources in the specified namespace. + * Gets all the distinct chainIds with datasources in the specified namespace. + * Note: This takes no consideration of which datasources are configured for indexing. * * @param namespaceId - The ENSNamespace identifier (e.g. 'mainnet', 'sepolia', 'holesky', * 'ens-test-env') From 5ece4f6442d68cc067ae04b3fbe908f4b6557891 Mon Sep 17 00:00:00 2001 From: "lightwalker.eth" <126201998+lightwalker-eth@users.noreply.github.com> Date: Mon, 25 Aug 2025 16:04:25 +0300 Subject: [PATCH 19/61] Refine docs --- packages/datasources/src/namespaces.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/datasources/src/namespaces.ts b/packages/datasources/src/namespaces.ts index b4ebd18d4..994aff9e6 100644 --- a/packages/datasources/src/namespaces.ts +++ b/packages/datasources/src/namespaces.ts @@ -73,14 +73,14 @@ export const maybeGetDatasource = ( ): Datasource | undefined => (getENSNamespace(namespaceId) as ENSNamespace)[datasourceName]; /** - * Returns the chain address for the specified namespace, datasource, and - * contract name, or undefined if it does not exist or is not a single chain address. + * Gets the chain address for the specified namespace, datasource, and + * contract name, or undefined if it is not defined or is not a single chain address. * - * This is useful when you want to retrieve the ChainAddress for an arbitrary contract + * This is useful when you want to retrieve a single ChainAddress for an arbitrary contract * where it may or may not actually be defined. * - * @param namespaceId - The ENSNamespace identifier (e.g. - * 'mainnet', 'sepolia', 'holesky', 'ens-test-env') + * @param namespaceId - The ENSNamespace identifier (e.g. 'mainnet', 'sepolia', 'holesky', + * 'ens-test-env') * @param datasourceName - The name of the Datasource to search for contractName in * @param contractName - The name of the contract to retrieve the chain address for * @returns The ChainAddress for the given namespace, datasource, and contract From 6631cd8bbca72c6ae419cf9a9d33a09b5e26ac4d Mon Sep 17 00:00:00 2001 From: "lightwalker.eth" <126201998+lightwalker-eth@users.noreply.github.com> Date: Mon, 25 Aug 2025 19:56:01 +0300 Subject: [PATCH 20/61] Refine data model --- apps/ensindexer/src/handlers/Seaport.ts | 120 +++++++------- apps/ensindexer/src/lib/tokenscope-helpers.ts | 63 +++++++- .../ensnode-schema/src/tokenscope.schema.ts | 150 ++++++++---------- 3 files changed, 197 insertions(+), 136 deletions(-) diff --git a/apps/ensindexer/src/handlers/Seaport.ts b/apps/ensindexer/src/handlers/Seaport.ts index 864a710bd..9dccc83f7 100644 --- a/apps/ensindexer/src/handlers/Seaport.ts +++ b/apps/ensindexer/src/handlers/Seaport.ts @@ -1,33 +1,33 @@ import { Context } from "ponder:registry"; import schema from "ponder:schema"; -import { ItemType } from "@opensea/seaport-js/lib/constants"; +import { ItemType as SeaportItemType } from "@opensea/seaport-js/lib/constants"; import config from "@/config"; -import { sharedEventValues, upsertAccount } from "@/lib/db-helpers"; +import { upsertAccount } from "@/lib/db-helpers"; import { EventWithArgs } from "@/lib/ponder-helpers"; import { CurrencyIds, - Price, - TokenId, + OnchainEventRef, + SupportedNFT, + SupportedPayment, + SupportedSale, TokenType, TokenTypes, getCurrencyIdForContract, - getDomainIdByTokenId, getKnownTokenIssuer, - isKnownTokenIssuer, + makeSaleId, + makeTokenRef, } from "@/lib/tokenscope-helpers"; -import { ChainAddress, ChainId, ENSNamespaceId } from "@ensnode/datasources"; -import { NameSoldInsert } from "@ensnode/ensnode-schema"; -import type { Node } from "@ensnode/ensnode-sdk"; +import { ChainId, ENSNamespaceId } from "@ensnode/datasources"; +import { uint256ToHex32 } from "@ensnode/ensnode-sdk"; import { Address, Hex } from "viem"; -import { PrivateKeyToAccountErrorType } from "viem/accounts"; type SeaportOfferItem = { /** * The type of item in the offer. * For example, ERC20, ERC721, ERC1155, or NATIVE (ETH) */ - itemType: ItemType; + itemType: SeaportItemType; /** * The contract address of the token. @@ -60,7 +60,7 @@ type SeaportConsiderationItem = { * The type of item in the consideration. * For example, ERC20, ERC721, ERC1155, or NATIVE (ETH) */ - itemType: ItemType; + itemType: SeaportItemType; /** * The contract address of the token. @@ -137,17 +137,6 @@ interface SeaportOrderFulfilledEvent consideration: readonly SeaportConsiderationItem[]; }> {} -interface SupportedNFT { - tokenType: TokenType; - contract: ChainAddress; - tokenId: TokenId; - domainId: Node; -} - -interface SupportedPayment { - price: Price; -} - /** * Gets the supported TokenScope token type for a given Seaport item type. * @@ -155,10 +144,10 @@ interface SupportedPayment { * @returns the supported TokenScope token type for the given SeaPort item type, or null * if the item type is not supported */ -const getSupportedTokenType = (itemType: ItemType): TokenType | null => { - if (itemType === ItemType.ERC721) { +const getSupportedTokenType = (itemType: SeaportItemType): TokenType | null => { + if (itemType === SeaportItemType.ERC721) { return TokenTypes.ERC721; - } else if (itemType === ItemType.ERC1155) { + } else if (itemType === SeaportItemType.ERC1155) { return TokenTypes.ERC1155; } else { return null; @@ -224,11 +213,11 @@ const getSupportedPayment = ( } // validate the Seaport item type is supported and matches the currencyId - if (item.itemType === ItemType.NATIVE) { + if (item.itemType === SeaportItemType.NATIVE) { if (currencyId !== CurrencyIds.ETH) { return null; // Seaport item type doesn't match currencyId } - } else if (item.itemType === ItemType.ERC20) { + } else if (item.itemType === SeaportItemType.ERC20) { if (currencyId === CurrencyIds.ETH) { return null; // Seaport item type doesn't match currencyId } @@ -307,12 +296,32 @@ const consolidateSupportedPayments = (payments: SupportedPayment[]): SupportedPa } satisfies SupportedPayment; }; -interface SupportedSale { - nft: SupportedNFT; - payment: SupportedPayment; - seller: Address; - buyer: Address; -} +const buildOnchainEventRef = ( + chainId: ChainId, + event: SeaportOrderFulfilledEvent, +): OnchainEventRef => { + if (event.block.timestamp > BigInt(Number.MAX_SAFE_INTEGER)) { + throw new Error( + `Error building onchain event ref: block timestamp is too large: ${event.block.timestamp}`, + ); + } + if (event.block.number > BigInt(Number.MAX_SAFE_INTEGER)) { + throw new Error( + `Error building onchain event ref: block number is too large: ${event.block.number}`, + ); + } + const blockNumber = Number(event.block.number); + const timestamp = Number(event.block.timestamp); + + return { + eventId: makeSaleId(chainId, event.block.number, event.log.logIndex), + chainId, + blockNumber, + logIndex: event.log.logIndex, + timestamp, + transactionHash: event.transaction.hash, + } satisfies OnchainEventRef; +}; const getSupportedSale = ( namespaceId: ENSNamespaceId, @@ -346,6 +355,8 @@ const getSupportedSale = ( // offer is exactly 1 supported NFT and consideration consolidates to 1 supported payment // therefore the offerer is the seller and the recipient is the buyer return { + event: buildOnchainEventRef(chainId, event), + orderHash, nft: consolidatedOfferNFT, payment: consolidatedOfferPayment, seller: offerer, @@ -360,6 +371,8 @@ const getSupportedSale = ( // consideration is exactly 1 supported NFT and offer consolidates to 1 supported payment // therefore the recipient is the seller and the offerer is the buyer return { + event: buildOnchainEventRef(chainId, event), + orderHash, nft: consolidatedConsiderationNFT, payment: consolidatedConsiderationPayment, seller: recipient, @@ -374,33 +387,32 @@ const getSupportedSale = ( /** * Indexes a supported sale transaction */ -const indexSupportedSale = async ( - context: Context, - event: SeaportOrderFulfilledEvent, - sale: SupportedSale, -): Promise => { - // Ensure buyer and selleraccounts exist +const indexSupportedSale = async (context: Context, sale: SupportedSale): Promise => { + // Ensure buyer and seller accounts exist await upsertAccount(context, sale.seller); await upsertAccount(context, sale.buyer); - const saleData: NameSoldInsert = { - ...sharedEventValues(context.chain.id, event), - logIndex: event.log.logIndex, - chainId: context.chain.id, - orderHash: event.args.orderHash, - timestamp: event.block.timestamp, - fromOwnerId: sale.seller, - newOwnerId: sale.buyer, + const nameSoldRecord = { + id: sale.event.eventId, + chainId: sale.nft.contract.chainId, + blockNumber: sale.event.blockNumber, + logIndex: sale.event.logIndex, + transactionHash: sale.event.transactionHash, + orderHash: sale.orderHash, contractAddress: sale.nft.contract.address, - tokenId: sale.nft.tokenId.toString(), // TODO: store as a bigint? + tokenId: uint256ToHex32(sale.nft.tokenId), tokenType: sale.nft.tokenType, + tokenRef: makeTokenRef(sale.nft.contract.chainId, sale.nft.contract.address, sale.nft.tokenId), domainId: sale.nft.domainId, - currencyId: sale.payment.price.currency, - price: sale.payment.price.amount, - }; + buyer: sale.buyer, + seller: sale.seller, + currency: sale.payment.price.currency, + amount: sale.payment.price.amount, + timestamp: sale.event.timestamp, + } satisfies typeof schema.nameSales.$inferInsert; // Index the sale - await context.db.insert(schema.nameSold).values(saleData); + await context.db.insert(schema.nameSales).values(nameSoldRecord); }; /** @@ -415,6 +427,6 @@ export async function handleOrderFulfilled({ }) { const supportedSale = getSupportedSale(config.namespace, context.chain.id, event); if (supportedSale) { - await indexSupportedSale(context, event, supportedSale); + await indexSupportedSale(context, supportedSale); } } diff --git a/apps/ensindexer/src/lib/tokenscope-helpers.ts b/apps/ensindexer/src/lib/tokenscope-helpers.ts index ea1c410de..7d4914979 100644 --- a/apps/ensindexer/src/lib/tokenscope-helpers.ts +++ b/apps/ensindexer/src/lib/tokenscope-helpers.ts @@ -1,3 +1,4 @@ +import { Price, TokenId, TokenType } from "@/lib/tokenscope-helpers"; import { ChainAddress, ChainId, @@ -13,10 +14,11 @@ import { LINEANAMES_NODE, LabelHash, type Node, + UnixTimestamp, makeSubdomainNode, uint256ToHex32, } from "@ensnode/ensnode-sdk"; -import { Address, zeroAddress } from "viem"; +import { Address, Hex, zeroAddress } from "viem"; import { base, baseSepolia, @@ -243,6 +245,32 @@ export function getDomainIdByTokenId( return tokenIssuer.getDomainId(tokenId); } +/** + * Makes a unique and deterministic Sale Id. + * + * @example `${chainId}-${blockNumber}-${logIndex}` + * + * @param chainId + * @param blockNumber + * @param logIndex + * @returns a unique and deterministic Sale Id + */ +export const makeSaleId = (chainId: ChainId, blockNumber: bigint, logIndex: number) => + [chainId.toString(), blockNumber.toString(), logIndex.toString()].join("-"); + +/** + * Makes a unique and deterministic TokenRef. + * + * @example `${chainId}-${contractAddress}-${tokenId}` + * + * @param chainId + * @param contractAddress + * @param tokenId + * @returns a unique and deterministic TokenRef + */ +export const makeTokenRef = (chainId: ChainId, contractAddress: Address, tokenId: TokenId) => + `${chainId}-${contractAddress}-${uint256ToHex32(tokenId)}`; + // TODO: Add support for WETH /** * Identifiers for supported currencies. @@ -434,3 +462,36 @@ export const getCurrencyIdForContract = ( return null; }; +export interface SupportedSale { + /** + * Event.id set as the unique and deterministic identifier of the onchain event + * associated with the sale. + * + * Composite key format: "{chainId}-{blockNumber}-{logIndex}" (e.g., "1-1234567-5"). + * + * @example "1-1234567-5" + */ + event: OnchainEventRef; + orderHash: Hex; + nft: SupportedNFT; + payment: SupportedPayment; + seller: Address; + buyer: Address; +} +export interface OnchainEventRef { + eventId: string; + chainId: ChainId; + blockNumber: number; + logIndex: number; + timestamp: UnixTimestamp; + transactionHash: Hex; +} +export interface SupportedPayment { + price: Price; +} +export interface SupportedNFT { + tokenType: TokenType; + contract: ChainAddress; + tokenId: TokenId; + domainId: Node; +} diff --git a/packages/ensnode-schema/src/tokenscope.schema.ts b/packages/ensnode-schema/src/tokenscope.schema.ts index c9655e7ca..c0f598e41 100644 --- a/packages/ensnode-schema/src/tokenscope.schema.ts +++ b/packages/ensnode-schema/src/tokenscope.schema.ts @@ -1,128 +1,116 @@ import schema from "ponder:schema"; import { index, onchainTable } from "ponder"; -const sharedEventColumns = (t: any) => ({ - /** - * The unique identifier of the event. - * - * Composite key format: "{chainId}-{blockNumber}-{logIndex}" (e.g., "1-1234567-5") - */ - id: t.text().primaryKey(), - - /** - * The block number where this event was emitted. - */ - blockNumber: t.integer().notNull(), - - /** - * The log index position of this event within its containing block. - * Determines event ordering when multiple events occur in the same block. - */ - logIndex: t.integer().notNull(), - - /** - * The transaction hash that generated this event. - */ - transactionID: t.hex().notNull(), - - /** - * The blockchain network identifier where this event occurred. - */ - chainId: t.integer().notNull(), -}); - -export const nameSold = onchainTable( - "name_sold", +export const nameSales = onchainTable( + "ext_name_sales", (t) => ({ - ...sharedEventColumns(t), - /** - * The account that previously owned and sold the domain. + * Unique and deterministic identifier of the onchain event associated with the sale. * - * Must have been the verified owner of the domain (domainId) and NFT (contractAddress + tokenId) - * before this sale. Received the payment specified by 'price' + 'currency' from newOwnerId. + * Composite key format: "{chainId}-{blockNumber}-{logIndex}" (e.g., "1-1234567-5") */ - fromOwnerId: t.hex().notNull(), + id: t.text().primaryKey(), /** - * The account that purchased and now owns the domain. - * - * Became the new owner of the domain (domainId) and its NFT representation (contractAddress + tokenId) - * after paying the amount specified by 'price' + 'currency' to fromOwnerId. + * The chain where the sale occurred. */ - newOwnerId: t.hex().notNull(), + chainId: t.integer().notNull(), /** - * Currency id of the payment (ETH, USDC or DAI). - * - * Works in conjunction with the 'price' field to define the payment. + * The block number on chainId where the sale occurred. */ - currencyId: t.text().notNull(), + blockNumber: t.integer().notNull(), /** - * The payment amount denominated in the smallest unit of the currency specified in 'currency' field. - * - * Amount interpretation depends on currency: - * - ETH/WETH: Amount in wei (1 ETH = 10^18 wei) - * - USDC: Amount in micro-units (1 USDC = 10^6 units) - * - DAI: Amount in wei-equivalent (1 DAI = 10^18 units) - * - * This value MUST be read together with 'currency' to determine actual payment value. + * The log index position of the sale event within blockNumber. + */ + logIndex: t.integer().notNull(), + + /** + * The EVM transaction hash on chainId associated with the sale. */ - price: t.bigint().notNull(), + transactionHash: t.hex().notNull(), /** - * Seaport protocol order hash identifying this specific sale order. - * Generated from order components (considerations, offers, etc.) to create unique order reference. + * The order hash of the sale order within contractAddress. */ orderHash: t.hex().notNull(), /** - * The ID of the token being sold within the contractAddress. + * The address of the contract on chainId that manages tokenId. + */ + contractAddress: t.hex().notNull(), + + /** + * The tokenId managed by contractAddress that was sold. + * + * Combined with `chainId` and `contractAddress`, creates unique NFT identifier + * for the domain being transferred. * - * Combined with 'contractAddress', creates unique NFT identifier for the domain being transferred. * Interpretation depends on 'tokenType': * - ERC721: Unique token within contract * - ERC1155: Token type identifier (multiple copies may exist) + * + * The tokenId is formatted as a hex string representing a uint256. */ tokenId: t.text().notNull(), /** - * The contract address of the token being sold. - * - * Works with 'tokenId' to uniquely identify the NFT representing the domain. - * Contract type must match 'tokenType' field (ERC721 or ERC1155). - * Address validity depends on 'chainId' where transaction occurred. + * The type of token that was sold (ERC721 or ERC1155). */ - contractAddress: t.hex().notNull(), + tokenType: t.text().notNull(), /** - * The namehash of the ENS domain being sold. + * Unique and deterministic identifier of token that was sold. * - * Links this sale to the specific ENS domain. + * Composite key format: "{chainId}-{contractAddress}-{tokenId}" + */ + tokenRef: t.text().notNull(), + + /** + * The namehash of the ENS domain that was sold. */ domainId: t.hex().notNull(), /** - * Unix timestamp of when the domain sale occurred (block timestamp). + * The account that bought the token controlling ownership of domainId from + * the seller for the amount of currency associated with the sale. + */ + buyer: t.hex().notNull(), + + /** + * The account that sold the token controlling ownership of domainId to + * buyer for the amount of currency associated with the sale. + */ + seller: t.hex().notNull(), + + /** + * Currency of the payment (ETH, USDC or DAI) from buyer to seller in exchange for tokenId. + */ + currency: t.text().notNull(), + + /** + * The amount of currency paid from buyer to seller in exchange for tokenId. * - * Corresponds to the timestamp of the block identified by 'blockNumber' + 'chainId'. + * Denominated in the smallest unit of currency. + * + * Amount interpretation depends on currency: + * - ETH/WETH: Amount in wei (1 ETH = 10^18 wei) + * - USDC: Amount in micro-units (1 USDC = 10^6 units) + * - DAI: Amount in wei-equivalent (1 DAI = 10^18 units) */ - timestamp: t.bigint().notNull(), + amount: t.bigint().notNull(), /** - * The type of token being sold (ERC721 or ERC1155). + * Unix timestamp of the block timestamp when the sale occurred. */ - tokenType: t.text().notNull(), + timestamp: t.integer().notNull(), }), (t) => ({ - idx_from: index().on(t.fromOwnerId), - idx_to: index().on(t.newOwnerId), - idx_domain: index().on(t.domainId), - idx_compound: index().on(t.fromOwnerId, t.id), - idx_created: index().on(t.timestamp), + idx_domainId: index().on(t.domainId), + idx_tokenRef: index().on(t.tokenRef), + idx_buyer: index().on(t.buyer), + idx_seller: index().on(t.seller), + idx_timestamp: index().on(t.timestamp), }), ); - -export type NameSoldInsert = typeof schema.nameSold.$inferInsert; -export type NameSold = typeof schema.nameSold.$inferSelect; From eb075018afc8ac0478946b4a444d820e903fcd0b Mon Sep 17 00:00:00 2001 From: "lightwalker.eth" <126201998+lightwalker-eth@users.noreply.github.com> Date: Mon, 25 Aug 2025 20:01:42 +0300 Subject: [PATCH 21/61] Refactorings --- apps/ensindexer/src/handlers/Seaport.ts | 4 +- apps/ensindexer/src/lib/tokenscope-helpers.ts | 45 ++++++++++--------- 2 files changed, 26 insertions(+), 23 deletions(-) diff --git a/apps/ensindexer/src/handlers/Seaport.ts b/apps/ensindexer/src/handlers/Seaport.ts index 9dccc83f7..d8dd2d491 100644 --- a/apps/ensindexer/src/handlers/Seaport.ts +++ b/apps/ensindexer/src/handlers/Seaport.ts @@ -15,7 +15,7 @@ import { TokenTypes, getCurrencyIdForContract, getKnownTokenIssuer, - makeSaleId, + makeEventId, makeTokenRef, } from "@/lib/tokenscope-helpers"; import { ChainId, ENSNamespaceId } from "@ensnode/datasources"; @@ -314,7 +314,7 @@ const buildOnchainEventRef = ( const timestamp = Number(event.block.timestamp); return { - eventId: makeSaleId(chainId, event.block.number, event.log.logIndex), + eventId: makeEventId(chainId, event.block.number, event.log.logIndex), chainId, blockNumber, logIndex: event.log.logIndex, diff --git a/apps/ensindexer/src/lib/tokenscope-helpers.ts b/apps/ensindexer/src/lib/tokenscope-helpers.ts index 7d4914979..08fda05b4 100644 --- a/apps/ensindexer/src/lib/tokenscope-helpers.ts +++ b/apps/ensindexer/src/lib/tokenscope-helpers.ts @@ -1,4 +1,3 @@ -import { Price, TokenId, TokenType } from "@/lib/tokenscope-helpers"; import { ChainAddress, ChainId, @@ -246,16 +245,16 @@ export function getDomainIdByTokenId( } /** - * Makes a unique and deterministic Sale Id. + * Makes a unique and deterministic event id. * * @example `${chainId}-${blockNumber}-${logIndex}` * * @param chainId * @param blockNumber * @param logIndex - * @returns a unique and deterministic Sale Id + * @returns a unique and deterministic event id. */ -export const makeSaleId = (chainId: ChainId, blockNumber: bigint, logIndex: number) => +export const makeEventId = (chainId: ChainId, blockNumber: bigint, logIndex: number) => [chainId.toString(), blockNumber.toString(), logIndex.toString()].join("-"); /** @@ -462,6 +461,27 @@ export const getCurrencyIdForContract = ( return null; }; + +export interface OnchainEventRef { + eventId: string; + chainId: ChainId; + blockNumber: number; + logIndex: number; + timestamp: UnixTimestamp; + transactionHash: Hex; +} + +export interface SupportedNFT { + tokenType: TokenType; + contract: ChainAddress; + tokenId: TokenId; + domainId: Node; +} + +export interface SupportedPayment { + price: Price; +} + export interface SupportedSale { /** * Event.id set as the unique and deterministic identifier of the onchain event @@ -478,20 +498,3 @@ export interface SupportedSale { seller: Address; buyer: Address; } -export interface OnchainEventRef { - eventId: string; - chainId: ChainId; - blockNumber: number; - logIndex: number; - timestamp: UnixTimestamp; - transactionHash: Hex; -} -export interface SupportedPayment { - price: Price; -} -export interface SupportedNFT { - tokenType: TokenType; - contract: ChainAddress; - tokenId: TokenId; - domainId: Node; -} From 5e1e97ea5e9aa4bc7a3512f777efac6c6d1113ba Mon Sep 17 00:00:00 2001 From: "lightwalker.eth" <126201998+lightwalker-eth@users.noreply.github.com> Date: Mon, 25 Aug 2025 20:09:08 +0300 Subject: [PATCH 22/61] Refactorings --- apps/ensindexer/src/handlers/Seaport.ts | 36 +------------------ apps/ensindexer/src/lib/tokenscope-helpers.ts | 34 ++++++++++++++++++ 2 files changed, 35 insertions(+), 35 deletions(-) diff --git a/apps/ensindexer/src/handlers/Seaport.ts b/apps/ensindexer/src/handlers/Seaport.ts index d8dd2d491..d751e8877 100644 --- a/apps/ensindexer/src/handlers/Seaport.ts +++ b/apps/ensindexer/src/handlers/Seaport.ts @@ -1,9 +1,7 @@ import { Context } from "ponder:registry"; -import schema from "ponder:schema"; import { ItemType as SeaportItemType } from "@opensea/seaport-js/lib/constants"; import config from "@/config"; -import { upsertAccount } from "@/lib/db-helpers"; import { EventWithArgs } from "@/lib/ponder-helpers"; import { CurrencyIds, @@ -15,11 +13,10 @@ import { TokenTypes, getCurrencyIdForContract, getKnownTokenIssuer, + indexSupportedSale, makeEventId, - makeTokenRef, } from "@/lib/tokenscope-helpers"; import { ChainId, ENSNamespaceId } from "@ensnode/datasources"; -import { uint256ToHex32 } from "@ensnode/ensnode-sdk"; import { Address, Hex } from "viem"; type SeaportOfferItem = { @@ -384,37 +381,6 @@ const getSupportedSale = ( } }; -/** - * Indexes a supported sale transaction - */ -const indexSupportedSale = async (context: Context, sale: SupportedSale): Promise => { - // Ensure buyer and seller accounts exist - await upsertAccount(context, sale.seller); - await upsertAccount(context, sale.buyer); - - const nameSoldRecord = { - id: sale.event.eventId, - chainId: sale.nft.contract.chainId, - blockNumber: sale.event.blockNumber, - logIndex: sale.event.logIndex, - transactionHash: sale.event.transactionHash, - orderHash: sale.orderHash, - contractAddress: sale.nft.contract.address, - tokenId: uint256ToHex32(sale.nft.tokenId), - tokenType: sale.nft.tokenType, - tokenRef: makeTokenRef(sale.nft.contract.chainId, sale.nft.contract.address, sale.nft.tokenId), - domainId: sale.nft.domainId, - buyer: sale.buyer, - seller: sale.seller, - currency: sale.payment.price.currency, - amount: sale.payment.price.amount, - timestamp: sale.event.timestamp, - } satisfies typeof schema.nameSales.$inferInsert; - - // Index the sale - await context.db.insert(schema.nameSales).values(nameSoldRecord); -}; - /** * Handles each Seaport OrderFulfilled event */ diff --git a/apps/ensindexer/src/lib/tokenscope-helpers.ts b/apps/ensindexer/src/lib/tokenscope-helpers.ts index 08fda05b4..030ea420c 100644 --- a/apps/ensindexer/src/lib/tokenscope-helpers.ts +++ b/apps/ensindexer/src/lib/tokenscope-helpers.ts @@ -1,3 +1,6 @@ +import { Context } from "ponder:registry"; +import schema from "ponder:schema"; +import { upsertAccount } from "@/lib/db-helpers"; import { ChainAddress, ChainId, @@ -498,3 +501,34 @@ export interface SupportedSale { seller: Address; buyer: Address; } + +/** + * Indexes a supported sale transaction + */ +export const indexSupportedSale = async (context: Context, sale: SupportedSale): Promise => { + // Ensure buyer and seller accounts exist + await upsertAccount(context, sale.seller); + await upsertAccount(context, sale.buyer); + + const nameSoldRecord = { + id: sale.event.eventId, + chainId: sale.nft.contract.chainId, + blockNumber: sale.event.blockNumber, + logIndex: sale.event.logIndex, + transactionHash: sale.event.transactionHash, + orderHash: sale.orderHash, + contractAddress: sale.nft.contract.address, + tokenId: uint256ToHex32(sale.nft.tokenId), + tokenType: sale.nft.tokenType, + tokenRef: makeTokenRef(sale.nft.contract.chainId, sale.nft.contract.address, sale.nft.tokenId), + domainId: sale.nft.domainId, + buyer: sale.buyer, + seller: sale.seller, + currency: sale.payment.price.currency, + amount: sale.payment.price.amount, + timestamp: sale.event.timestamp, + } satisfies typeof schema.nameSales.$inferInsert; + + // Index the sale + await context.db.insert(schema.nameSales).values(nameSoldRecord); +}; From 694f225bdc1f4612803e316f8979a9cf215723eb Mon Sep 17 00:00:00 2001 From: "lightwalker.eth" <126201998+lightwalker-eth@users.noreply.github.com> Date: Mon, 25 Aug 2025 20:25:56 +0300 Subject: [PATCH 23/61] typecheck --- apps/ensadmin/src/components/indexing-status/block-refs.tsx | 3 ++- .../src/components/indexing-status/indexing-timeline.tsx | 3 ++- apps/ensadmin/src/lib/namespace-utils.ts | 3 ++- .../api/lib/acceleration/known-ensip-19-reverse-resolvers.ts | 3 +-- .../api/lib/acceleration/known-offchain-lookup-resolver.ts | 4 ++-- .../api/lib/acceleration/known-onchain-static-resolver.ts | 3 +-- .../lib/acceleration/resolver-records-indexed-on-chain.ts | 4 ++-- apps/ensindexer/src/api/lib/find-resolver.ts | 4 ++-- apps/ensindexer/src/api/lib/get-records-from-index.ts | 2 +- apps/ensindexer/src/api/lib/public-client.ts | 2 +- .../api/lib/resolution/multichain-primary-name-resolution.ts | 3 +-- apps/ensindexer/src/config/config.schema.ts | 4 ++-- apps/ensindexer/src/config/derived-params.ts | 3 ++- apps/ensindexer/src/config/serialized-types.ts | 3 ++- apps/ensindexer/src/config/types.ts | 4 ++-- .../ensindexer/src/indexing-status/ponder-metadata/chains.ts | 2 +- apps/ensindexer/src/lib/ponder-helpers.ts | 5 ++--- packages/ensnode-sdk/src/ens/coin-type.ts | 2 +- packages/ensnode-sdk/src/ensindexer/api/zod-schemas.ts | 3 ++- packages/ensnode-sdk/src/ensindexer/config/serialize.ts | 2 +- .../ensnode-sdk/src/ensindexer/config/serialized-types.ts | 3 ++- packages/ensnode-sdk/src/ensindexer/config/types.ts | 3 +-- .../ensnode-sdk/src/ensindexer/indexing-status/helpers.ts | 3 ++- .../ensnode-sdk/src/ensindexer/indexing-status/serialize.ts | 4 +++- packages/ensnode-sdk/src/ensindexer/indexing-status/types.ts | 3 ++- .../src/ensindexer/indexing-status/zod-schemas.ts | 3 ++- packages/ensnode-sdk/src/resolution/types.ts | 2 +- 27 files changed, 45 insertions(+), 38 deletions(-) diff --git a/apps/ensadmin/src/components/indexing-status/block-refs.tsx b/apps/ensadmin/src/components/indexing-status/block-refs.tsx index 398d0d528..767657a1e 100644 --- a/apps/ensadmin/src/components/indexing-status/block-refs.tsx +++ b/apps/ensadmin/src/components/indexing-status/block-refs.tsx @@ -4,7 +4,8 @@ import { RelativeTime } from "@/components/datetime-utils"; import { getBlockExplorerUrlForBlock } from "@/lib/namespace-utils"; -import { BlockRef, ChainId } from "@ensnode/ensnode-sdk"; +import { ChainId } from "@ensnode/datasources"; +import { BlockRef } from "@ensnode/ensnode-sdk"; import { fromUnixTime } from "date-fns"; import { ExternalLink } from "lucide-react"; diff --git a/apps/ensadmin/src/components/indexing-status/indexing-timeline.tsx b/apps/ensadmin/src/components/indexing-status/indexing-timeline.tsx index ff5674c56..a38ab2d1a 100644 --- a/apps/ensadmin/src/components/indexing-status/indexing-timeline.tsx +++ b/apps/ensadmin/src/components/indexing-status/indexing-timeline.tsx @@ -4,7 +4,8 @@ import { ChainName } from "@/components/chains/ChainName"; import { cn } from "@/lib/utils"; -import { ChainId, ChainIndexingStatusIds } from "@ensnode/ensnode-sdk"; +import { ChainId } from "@ensnode/datasources"; +import { ChainIndexingStatusIds } from "@ensnode/ensnode-sdk"; import { intlFormat } from "date-fns"; import { BlockRefViewModel } from "@/components/indexing-status/block-refs"; diff --git a/apps/ensadmin/src/lib/namespace-utils.ts b/apps/ensadmin/src/lib/namespace-utils.ts index cefe1732a..1755eecaa 100644 --- a/apps/ensadmin/src/lib/namespace-utils.ts +++ b/apps/ensadmin/src/lib/namespace-utils.ts @@ -1,10 +1,11 @@ import { + ChainId, DatasourceNames, ENSNamespaceId, ENSNamespaceIds, getDatasource, } from "@ensnode/datasources"; -import { ChainId, Name } from "@ensnode/ensnode-sdk"; +import { Name } from "@ensnode/ensnode-sdk"; import { Address } from "viem"; import { anvil, diff --git a/apps/ensindexer/src/api/lib/acceleration/known-ensip-19-reverse-resolvers.ts b/apps/ensindexer/src/api/lib/acceleration/known-ensip-19-reverse-resolvers.ts index c5f0c5648..715f48ea7 100644 --- a/apps/ensindexer/src/api/lib/acceleration/known-ensip-19-reverse-resolvers.ts +++ b/apps/ensindexer/src/api/lib/acceleration/known-ensip-19-reverse-resolvers.ts @@ -1,6 +1,5 @@ import config from "@/config"; -import { DatasourceNames, maybeGetDatasource } from "@ensnode/datasources"; -import { ChainId } from "@ensnode/ensnode-sdk"; +import { ChainId, DatasourceNames, maybeGetDatasource } from "@ensnode/datasources"; import { Address } from "viem"; const rrRoot = maybeGetDatasource(config.namespace, DatasourceNames.ReverseResolverRoot); diff --git a/apps/ensindexer/src/api/lib/acceleration/known-offchain-lookup-resolver.ts b/apps/ensindexer/src/api/lib/acceleration/known-offchain-lookup-resolver.ts index 5ecb45591..86e317ba2 100644 --- a/apps/ensindexer/src/api/lib/acceleration/known-offchain-lookup-resolver.ts +++ b/apps/ensindexer/src/api/lib/acceleration/known-offchain-lookup-resolver.ts @@ -1,6 +1,6 @@ import config from "@/config"; -import { DatasourceNames, maybeGetDatasource } from "@ensnode/datasources"; -import { ChainId, PluginName } from "@ensnode/ensnode-sdk"; +import { ChainId, DatasourceNames, maybeGetDatasource } from "@ensnode/datasources"; +import { PluginName } from "@ensnode/ensnode-sdk"; import { Address, isAddressEqual } from "viem"; // NOTE: we know ensRoot is defined for all namespaces, so enforce that at runtime with ! diff --git a/apps/ensindexer/src/api/lib/acceleration/known-onchain-static-resolver.ts b/apps/ensindexer/src/api/lib/acceleration/known-onchain-static-resolver.ts index e13ae52a8..3e3d29364 100644 --- a/apps/ensindexer/src/api/lib/acceleration/known-onchain-static-resolver.ts +++ b/apps/ensindexer/src/api/lib/acceleration/known-onchain-static-resolver.ts @@ -1,6 +1,5 @@ import config from "@/config"; -import { DatasourceNames, maybeGetDatasource } from "@ensnode/datasources"; -import { ChainId } from "@ensnode/ensnode-sdk"; +import { ChainId, DatasourceNames, maybeGetDatasource } from "@ensnode/datasources"; import { Address } from "viem"; const rrRoot = maybeGetDatasource(config.namespace, DatasourceNames.ReverseResolverRoot); diff --git a/apps/ensindexer/src/api/lib/acceleration/resolver-records-indexed-on-chain.ts b/apps/ensindexer/src/api/lib/acceleration/resolver-records-indexed-on-chain.ts index 2c6f475da..49b5fc809 100644 --- a/apps/ensindexer/src/api/lib/acceleration/resolver-records-indexed-on-chain.ts +++ b/apps/ensindexer/src/api/lib/acceleration/resolver-records-indexed-on-chain.ts @@ -1,6 +1,6 @@ import config from "@/config"; -import { DatasourceNames, maybeGetDatasource } from "@ensnode/datasources"; -import { ChainId, PluginName } from "@ensnode/ensnode-sdk"; +import { ChainId, DatasourceNames, maybeGetDatasource } from "@ensnode/datasources"; +import { PluginName } from "@ensnode/ensnode-sdk"; // NOTE: we know ensRoot is defined for all namespaces, so enforce that at runtime with ! const ensRoot = maybeGetDatasource(config.namespace, DatasourceNames.ENSRoot)!; diff --git a/apps/ensindexer/src/api/lib/find-resolver.ts b/apps/ensindexer/src/api/lib/find-resolver.ts index 3a55b41cd..38a0297d2 100644 --- a/apps/ensindexer/src/api/lib/find-resolver.ts +++ b/apps/ensindexer/src/api/lib/find-resolver.ts @@ -1,6 +1,6 @@ import { db } from "ponder:api"; -import { DatasourceNames, getDatasource, getENSRootChainId } from "@ensnode/datasources"; -import { ChainId, type Name, type Node, PluginName, getNameHierarchy } from "@ensnode/ensnode-sdk"; +import { ChainId, DatasourceNames, getDatasource, getENSRootChainId } from "@ensnode/datasources"; +import { type Name, type Node, PluginName, getNameHierarchy } from "@ensnode/ensnode-sdk"; import { SpanStatusCode, trace } from "@opentelemetry/api"; import { type Address, diff --git a/apps/ensindexer/src/api/lib/get-records-from-index.ts b/apps/ensindexer/src/api/lib/get-records-from-index.ts index bcb09e910..026b10627 100644 --- a/apps/ensindexer/src/api/lib/get-records-from-index.ts +++ b/apps/ensindexer/src/api/lib/get-records-from-index.ts @@ -3,8 +3,8 @@ import { onchainStaticResolverImplementsDefaultAddress } from "@/api/lib/acceler import type { IndexedResolverRecords } from "@/api/lib/make-records-response"; import { withSpanAsync } from "@/lib/auto-span"; import { makeResolverId } from "@/lib/ids"; +import { ChainId } from "@ensnode/datasources"; import { - ChainId, DEFAULT_EVM_COIN_TYPE, Node, ResolverRecordsSelection, diff --git a/apps/ensindexer/src/api/lib/public-client.ts b/apps/ensindexer/src/api/lib/public-client.ts index 5900f1d49..64314b13c 100644 --- a/apps/ensindexer/src/api/lib/public-client.ts +++ b/apps/ensindexer/src/api/lib/public-client.ts @@ -1,5 +1,5 @@ import config from "@/config"; -import { ChainId } from "@ensnode/ensnode-sdk"; +import { ChainId } from "@ensnode/datasources"; import { http, createPublicClient } from "viem"; export function getPublicClient(chainId: ChainId) { diff --git a/apps/ensindexer/src/api/lib/resolution/multichain-primary-name-resolution.ts b/apps/ensindexer/src/api/lib/resolution/multichain-primary-name-resolution.ts index 93144066f..6488a4053 100644 --- a/apps/ensindexer/src/api/lib/resolution/multichain-primary-name-resolution.ts +++ b/apps/ensindexer/src/api/lib/resolution/multichain-primary-name-resolution.ts @@ -1,5 +1,4 @@ import { - type ChainId, type MultichainPrimaryNameResolutionArgs, type MultichainPrimaryNameResolutionResult, uniq, @@ -9,7 +8,7 @@ import { trace } from "@opentelemetry/api"; import { resolveReverse } from "@/api/lib/resolution/reverse-resolution"; import config from "@/config"; import { withActiveSpanAsync } from "@/lib/auto-span"; -import { DatasourceNames, getDatasource, maybeGetDatasource } from "@ensnode/datasources"; +import { type ChainId, DatasourceNames, getDatasource, maybeGetDatasource } from "@ensnode/datasources"; const tracer = trace.getTracer("multichain-primary-name-resolution"); diff --git a/apps/ensindexer/src/config/config.schema.ts b/apps/ensindexer/src/config/config.schema.ts index 0470dc17b..0483aa0c8 100644 --- a/apps/ensindexer/src/config/config.schema.ts +++ b/apps/ensindexer/src/config/config.schema.ts @@ -1,8 +1,8 @@ import { parse as parseConnectionString } from "pg-connection-string"; import { prettifyError, z } from "zod/v4"; -import { ENSNamespaceIds } from "@ensnode/datasources"; -import { type ChainId, PluginName, deserializeChainId, uniq } from "@ensnode/ensnode-sdk"; +import { type ChainId, ENSNamespaceIds } from "@ensnode/datasources"; +import { PluginName, deserializeChainId, uniq } from "@ensnode/ensnode-sdk"; import { makeUrlSchema } from "@ensnode/ensnode-sdk/internal"; import { diff --git a/apps/ensindexer/src/config/derived-params.ts b/apps/ensindexer/src/config/derived-params.ts index 91a6a574f..0827cdc63 100644 --- a/apps/ensindexer/src/config/derived-params.ts +++ b/apps/ensindexer/src/config/derived-params.ts @@ -1,7 +1,8 @@ import type { ENSIndexerConfig } from "@/config/types"; import { getENSNamespaceAsFullyDefinedAtCompileTime } from "@/lib/plugin-helpers"; import { getPlugin } from "@/plugins"; -import { ChainId, isSubgraphCompatible } from "@ensnode/ensnode-sdk"; +import { ChainId } from "@ensnode/datasources"; +import { isSubgraphCompatible } from "@ensnode/ensnode-sdk"; /** * Derive `indexedChainIds` configuration parameter and include it in diff --git a/apps/ensindexer/src/config/serialized-types.ts b/apps/ensindexer/src/config/serialized-types.ts index 35cebb3bc..904e5a99f 100644 --- a/apps/ensindexer/src/config/serialized-types.ts +++ b/apps/ensindexer/src/config/serialized-types.ts @@ -1,4 +1,5 @@ -import type { ChainId, ChainIdString, UrlString } from "@ensnode/ensnode-sdk"; +import { ChainId } from "@ensnode/datasources"; +import type { ChainIdString, UrlString } from "@ensnode/ensnode-sdk"; import type { ENSIndexerConfig, RpcConfig } from "./types"; /** diff --git a/apps/ensindexer/src/config/types.ts b/apps/ensindexer/src/config/types.ts index 09fee8a30..a31908bf5 100644 --- a/apps/ensindexer/src/config/types.ts +++ b/apps/ensindexer/src/config/types.ts @@ -1,5 +1,5 @@ -import type { ENSNamespaceId, ENSNamespaceIds } from "@ensnode/datasources"; -import type { Blockrange, ChainId, ChainIdString, PluginName } from "@ensnode/ensnode-sdk"; +import type { ChainId,ENSNamespaceId, ENSNamespaceIds } from "@ensnode/datasources"; +import type { Blockrange, ChainIdString, PluginName } from "@ensnode/ensnode-sdk"; /** * Configuration for a single RPC used by ENSIndexer. diff --git a/apps/ensindexer/src/indexing-status/ponder-metadata/chains.ts b/apps/ensindexer/src/indexing-status/ponder-metadata/chains.ts index 3a67b44ed..203dc7c8a 100644 --- a/apps/ensindexer/src/indexing-status/ponder-metadata/chains.ts +++ b/apps/ensindexer/src/indexing-status/ponder-metadata/chains.ts @@ -11,9 +11,9 @@ * all come together to form a single view about a chain's indexing status. */ +import { ChainId } from "@ensnode/datasources"; import { type BlockRef, - type ChainId, type ChainIndexingBackfillStatus, type ChainIndexingCompletedStatus, type ChainIndexingFollowingStatus, diff --git a/apps/ensindexer/src/lib/ponder-helpers.ts b/apps/ensindexer/src/lib/ponder-helpers.ts index 4439c1e2c..b81f38a6f 100644 --- a/apps/ensindexer/src/lib/ponder-helpers.ts +++ b/apps/ensindexer/src/lib/ponder-helpers.ts @@ -10,12 +10,11 @@ import type { ChainConfig } from "ponder"; import { Address, PublicClient } from "viem"; import * as z from "zod/v4"; -import { ContractConfig } from "@ensnode/datasources"; -import { EnsRainbowApiClient } from "@ensnode/ensrainbow-sdk"; +import { ChainId, ContractConfig } from "@ensnode/datasources"; import type { BlockInfo, PonderStatus } from "@ensnode/ponder-metadata"; import { ENSIndexerConfig } from "@/config/types"; -import type { Blockrange, ChainId } from "@ensnode/ensnode-sdk"; +import type { Blockrange } from "@ensnode/ensnode-sdk"; export type EventWithArgs = {}> = Omit & { args: ARGS; diff --git a/packages/ensnode-sdk/src/ens/coin-type.ts b/packages/ensnode-sdk/src/ens/coin-type.ts index 3b97ad67b..7443bc943 100644 --- a/packages/ensnode-sdk/src/ens/coin-type.ts +++ b/packages/ensnode-sdk/src/ens/coin-type.ts @@ -3,7 +3,7 @@ import { coinTypeToEvmChainId as _coinTypeToEvmChainId, evmChainIdToCoinType as _evmChainIdToCoinType, } from "@ensdomains/address-encoder/utils"; -import { ChainId } from "../shared"; +import { ChainId } from "@ensnode/datasources"; // re-export CoinType and EvmCoinType from @ensdomains/address-encoder // so consumers don't need it as a dependency diff --git a/packages/ensnode-sdk/src/ensindexer/api/zod-schemas.ts b/packages/ensnode-sdk/src/ensindexer/api/zod-schemas.ts index 67d3c77bd..074c8722b 100644 --- a/packages/ensnode-sdk/src/ensindexer/api/zod-schemas.ts +++ b/packages/ensnode-sdk/src/ensindexer/api/zod-schemas.ts @@ -1,10 +1,11 @@ import { Address, getAddress } from "viem"; import { z } from "zod/v4"; +import { ChainId } from "@ensnode/datasources"; import { CoinType, DEFAULT_EVM_CHAIN_ID } from "../../ens/coin-type"; import { Name } from "../../ens/types"; import { ResolverRecordsSelection, isSelectionEmpty } from "../../resolution"; -import { ChainId, isNormalized } from "../../shared"; +import { isNormalized } from "../../shared"; import { makeDurationSchema } from "../../shared/zod-schemas"; const toName = (val: string) => val as Name; diff --git a/packages/ensnode-sdk/src/ensindexer/config/serialize.ts b/packages/ensnode-sdk/src/ensindexer/config/serialize.ts index be01bd800..15f5e28f1 100644 --- a/packages/ensnode-sdk/src/ensindexer/config/serialize.ts +++ b/packages/ensnode-sdk/src/ensindexer/config/serialize.ts @@ -1,4 +1,4 @@ -import { ChainId } from "../../shared"; +import { ChainId } from "@ensnode/datasources"; import { serializeUrl } from "../../shared/serialize"; import { SerializedENSIndexerPublicConfig, SerializedIndexedChainIds } from "./serialized-types"; import { ENSIndexerPublicConfig } from "./types"; diff --git a/packages/ensnode-sdk/src/ensindexer/config/serialized-types.ts b/packages/ensnode-sdk/src/ensindexer/config/serialized-types.ts index 2882c2443..7fd7337f9 100644 --- a/packages/ensnode-sdk/src/ensindexer/config/serialized-types.ts +++ b/packages/ensnode-sdk/src/ensindexer/config/serialized-types.ts @@ -1,4 +1,5 @@ -import type { ChainId, UrlString } from "../../shared"; +import type { ChainId } from "@ensnode/datasources"; +import type { UrlString } from "../../shared"; import type { ENSIndexerPublicConfig } from "./types"; export type SerializedIndexedChainIds = Array; diff --git a/packages/ensnode-sdk/src/ensindexer/config/types.ts b/packages/ensnode-sdk/src/ensindexer/config/types.ts index 7f84cde21..5a62e00d9 100644 --- a/packages/ensnode-sdk/src/ensindexer/config/types.ts +++ b/packages/ensnode-sdk/src/ensindexer/config/types.ts @@ -1,5 +1,4 @@ -import type { ENSNamespaceId } from "../../ens"; -import type { ChainId } from "../../shared"; +import type { ChainId, ENSNamespaceId } from "@ensnode/datasources"; /** * A PluginName is a unique id for a 'plugin': we use the notion of diff --git a/packages/ensnode-sdk/src/ensindexer/indexing-status/helpers.ts b/packages/ensnode-sdk/src/ensindexer/indexing-status/helpers.ts index 757849728..e4b577436 100644 --- a/packages/ensnode-sdk/src/ensindexer/indexing-status/helpers.ts +++ b/packages/ensnode-sdk/src/ensindexer/indexing-status/helpers.ts @@ -1,4 +1,5 @@ -import { BlockRef, ChainId, Duration, UnixTimestamp } from "../../shared"; +import { ChainId } from "@ensnode/datasources"; +import { BlockRef, Duration, UnixTimestamp } from "../../shared"; import { ChainIndexingActiveStatus, ChainIndexingCompletedStatus, diff --git a/packages/ensnode-sdk/src/ensindexer/indexing-status/serialize.ts b/packages/ensnode-sdk/src/ensindexer/indexing-status/serialize.ts index 93445ade5..e7e37fe2e 100644 --- a/packages/ensnode-sdk/src/ensindexer/indexing-status/serialize.ts +++ b/packages/ensnode-sdk/src/ensindexer/indexing-status/serialize.ts @@ -1,4 +1,6 @@ -import { ChainId, ChainIdString, serializeChainId } from "../../shared"; +import { ChainId } from "@ensnode/datasources"; +import { ChainIdString, serializeChainId } from "../../shared"; + import { SerializedENSIndexerOverallIndexingBackfillStatus, SerializedENSIndexerOverallIndexingCompletedStatus, diff --git a/packages/ensnode-sdk/src/ensindexer/indexing-status/types.ts b/packages/ensnode-sdk/src/ensindexer/indexing-status/types.ts index fa7c21ac4..5042f6bb4 100644 --- a/packages/ensnode-sdk/src/ensindexer/indexing-status/types.ts +++ b/packages/ensnode-sdk/src/ensindexer/indexing-status/types.ts @@ -1,4 +1,5 @@ -import type { BlockRef, ChainId, Duration, UnixTimestamp } from "../../shared"; +import type { ChainId } from "@ensnode/datasources"; +import type { BlockRef, Duration, UnixTimestamp } from "../../shared"; export const ChainIndexingStatusIds = { Unstarted: "unstarted", diff --git a/packages/ensnode-sdk/src/ensindexer/indexing-status/zod-schemas.ts b/packages/ensnode-sdk/src/ensindexer/indexing-status/zod-schemas.ts index 27c039cc1..a73e6060c 100644 --- a/packages/ensnode-sdk/src/ensindexer/indexing-status/zod-schemas.ts +++ b/packages/ensnode-sdk/src/ensindexer/indexing-status/zod-schemas.ts @@ -6,8 +6,9 @@ * The only way to share Zod schemas is to re-export them from * `./src/internal.ts` file. */ +import { ChainId } from "@ensnode/datasources"; import z from "zod/v4"; -import { ChainId, deserializeChainId } from "../../shared"; +import { deserializeChainId } from "../../shared"; import * as blockRef from "../../shared/block-ref"; import { makeBlockRefSchema, diff --git a/packages/ensnode-sdk/src/resolution/types.ts b/packages/ensnode-sdk/src/resolution/types.ts index 36aae5ec4..d338c7123 100644 --- a/packages/ensnode-sdk/src/resolution/types.ts +++ b/packages/ensnode-sdk/src/resolution/types.ts @@ -1,7 +1,7 @@ import { Address } from "viem"; +import { ChainId } from "@ensnode/datasources"; import type { Name } from "../ens"; -import { ChainId } from "../shared"; import { ResolverRecordsResponse } from "./resolver-records-response"; import type { ResolverRecordsSelection } from "./resolver-records-selection"; From 1b1f2d1bba185f60cf053f28dc44391507aa3be5 Mon Sep 17 00:00:00 2001 From: "lightwalker.eth" <126201998+lightwalker-eth@users.noreply.github.com> Date: Mon, 25 Aug 2025 20:31:31 +0300 Subject: [PATCH 24/61] Update changeset --- .changeset/six-pillows-pump.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/six-pillows-pump.md b/.changeset/six-pillows-pump.md index 5fa8971d8..358ebed02 100644 --- a/.changeset/six-pillows-pump.md +++ b/.changeset/six-pillows-pump.md @@ -4,4 +4,4 @@ "ensindexer": minor --- -Added new Plugin: TokenScope. This Plugin for now will index Seaport-Sales across all other name-plugins we support (ENS, 3dns etc) +Initial launch of ENS TokenScope with support for indexing seaport sales. From 6841aa85d8c6ae75f932d26c5bd4e759de11c22a Mon Sep 17 00:00:00 2001 From: "lightwalker.eth" <126201998+lightwalker-eth@users.noreply.github.com> Date: Mon, 25 Aug 2025 20:32:14 +0300 Subject: [PATCH 25/61] lint --- apps/ensindexer/src/api/lib/get-records-from-index.ts | 6 +----- .../lib/resolution/multichain-primary-name-resolution.ts | 7 ++++++- apps/ensindexer/src/config/types.ts | 2 +- 3 files changed, 8 insertions(+), 7 deletions(-) diff --git a/apps/ensindexer/src/api/lib/get-records-from-index.ts b/apps/ensindexer/src/api/lib/get-records-from-index.ts index 026b10627..541ad92ab 100644 --- a/apps/ensindexer/src/api/lib/get-records-from-index.ts +++ b/apps/ensindexer/src/api/lib/get-records-from-index.ts @@ -4,11 +4,7 @@ import type { IndexedResolverRecords } from "@/api/lib/make-records-response"; import { withSpanAsync } from "@/lib/auto-span"; import { makeResolverId } from "@/lib/ids"; import { ChainId } from "@ensnode/datasources"; -import { - DEFAULT_EVM_COIN_TYPE, - Node, - ResolverRecordsSelection, -} from "@ensnode/ensnode-sdk"; +import { DEFAULT_EVM_COIN_TYPE, Node, ResolverRecordsSelection } from "@ensnode/ensnode-sdk"; import { trace } from "@opentelemetry/api"; import { Address } from "viem"; diff --git a/apps/ensindexer/src/api/lib/resolution/multichain-primary-name-resolution.ts b/apps/ensindexer/src/api/lib/resolution/multichain-primary-name-resolution.ts index 6488a4053..0c4a24837 100644 --- a/apps/ensindexer/src/api/lib/resolution/multichain-primary-name-resolution.ts +++ b/apps/ensindexer/src/api/lib/resolution/multichain-primary-name-resolution.ts @@ -8,7 +8,12 @@ import { trace } from "@opentelemetry/api"; import { resolveReverse } from "@/api/lib/resolution/reverse-resolution"; import config from "@/config"; import { withActiveSpanAsync } from "@/lib/auto-span"; -import { type ChainId, DatasourceNames, getDatasource, maybeGetDatasource } from "@ensnode/datasources"; +import { + type ChainId, + DatasourceNames, + getDatasource, + maybeGetDatasource, +} from "@ensnode/datasources"; const tracer = trace.getTracer("multichain-primary-name-resolution"); diff --git a/apps/ensindexer/src/config/types.ts b/apps/ensindexer/src/config/types.ts index a31908bf5..d75acd3a1 100644 --- a/apps/ensindexer/src/config/types.ts +++ b/apps/ensindexer/src/config/types.ts @@ -1,4 +1,4 @@ -import type { ChainId,ENSNamespaceId, ENSNamespaceIds } from "@ensnode/datasources"; +import type { ChainId, ENSNamespaceId, ENSNamespaceIds } from "@ensnode/datasources"; import type { Blockrange, ChainIdString, PluginName } from "@ensnode/ensnode-sdk"; /** From da5787cbf4b00298a7260a97ca29d795f5ad5c01 Mon Sep 17 00:00:00 2001 From: "lightwalker.eth" <126201998+lightwalker-eth@users.noreply.github.com> Date: Mon, 25 Aug 2025 20:33:26 +0300 Subject: [PATCH 26/61] Update changeset --- .changeset/six-pillows-pump.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/six-pillows-pump.md b/.changeset/six-pillows-pump.md index 358ebed02..32b241430 100644 --- a/.changeset/six-pillows-pump.md +++ b/.changeset/six-pillows-pump.md @@ -4,4 +4,4 @@ "ensindexer": minor --- -Initial launch of ENS TokenScope with support for indexing seaport sales. +Initial launch of ENS TokenScope with support for indexing Seaport sales. From cc8c909c76f67569373bc15b15594943025d63f7 Mon Sep 17 00:00:00 2001 From: "lightwalker.eth" <126201998+lightwalker-eth@users.noreply.github.com> Date: Mon, 25 Aug 2025 20:36:46 +0300 Subject: [PATCH 27/61] Revert changes to shell script executable permissions --- .github/scripts/promote_ensadmin.sh | 0 .github/scripts/run_ensindexer_healthcheck.sh | 0 apps/ensrainbow/download-rainbow-tables.sh | 0 apps/ensrainbow/scripts/download-ensrainbow-files.sh | 0 4 files changed, 0 insertions(+), 0 deletions(-) mode change 100644 => 100755 .github/scripts/promote_ensadmin.sh mode change 100644 => 100755 .github/scripts/run_ensindexer_healthcheck.sh mode change 100644 => 100755 apps/ensrainbow/download-rainbow-tables.sh mode change 100644 => 100755 apps/ensrainbow/scripts/download-ensrainbow-files.sh diff --git a/.github/scripts/promote_ensadmin.sh b/.github/scripts/promote_ensadmin.sh old mode 100644 new mode 100755 diff --git a/.github/scripts/run_ensindexer_healthcheck.sh b/.github/scripts/run_ensindexer_healthcheck.sh old mode 100644 new mode 100755 diff --git a/apps/ensrainbow/download-rainbow-tables.sh b/apps/ensrainbow/download-rainbow-tables.sh old mode 100644 new mode 100755 diff --git a/apps/ensrainbow/scripts/download-ensrainbow-files.sh b/apps/ensrainbow/scripts/download-ensrainbow-files.sh old mode 100644 new mode 100755 From cd53bf7e897fed6c0558a44420d3bbf15432ed20 Mon Sep 17 00:00:00 2001 From: "lightwalker.eth" <126201998+lightwalker-eth@users.noreply.github.com> Date: Mon, 25 Aug 2025 20:44:51 +0300 Subject: [PATCH 28/61] Advertise new tokenscope plugin --- .github/workflows/test_ci.yml | 2 +- apps/ensindexer/.env.local.example | 5 ++++- docs/ensnode.io/src/content/docs/docs/usage/api.mdx | 3 ++- .../ensnode-sdk/src/ensindexer/config/zod-schemas.test.ts | 2 +- terraform/main.tf | 4 ++-- 5 files changed, 10 insertions(+), 6 deletions(-) diff --git a/.github/workflows/test_ci.yml b/.github/workflows/test_ci.yml index 97c6f7017..450e7067b 100644 --- a/.github/workflows/test_ci.yml +++ b/.github/workflows/test_ci.yml @@ -59,7 +59,7 @@ jobs: # We use private RPC URLs from GitHub Secrets to avoid rate limits. # Public RPC URLs are used as fallbacks for repository forks # that don't have the relevant secrets configured. - PLUGINS: subgraph,basenames,lineanames,threedns,reverse-resolvers,referrals + PLUGINS: subgraph,basenames,lineanames,threedns,reverse-resolvers,referrals,tokenscope RPC_URL_1: ${{ secrets.MAINNET_RPC_URL || 'https://eth.drpc.org' }} RPC_URL_10: ${{ secrets.OPTIMISM_RPC_URL || 'https://optimism.drpc.org' }} RPC_URL_8453: ${{ secrets.BASE_RPC_URL || 'https://base.drpc.org' }} diff --git a/apps/ensindexer/.env.local.example b/apps/ensindexer/.env.local.example index 405c1a783..f4eeea66c 100644 --- a/apps/ensindexer/.env.local.example +++ b/apps/ensindexer/.env.local.example @@ -40,6 +40,7 @@ PORT=42069 # === ENS Namespace: Mainnet === # Ethereum Mainnet # - required if the configured namespace is mainnet +# - required by plugins: subgraph, reverse-resolvers, referrals, tokenscope RPC_URL_1= RPC_REQUEST_RATE_LIMIT_1=500 @@ -71,6 +72,7 @@ RPC_REQUEST_RATE_LIMIT_534352=500 # === ENS Namespace: Sepolia === # Ethereum Sepolia (public testnet) # - required if the configured namespace is sepolia +# - required by plugins: subgraph, reverse-resolvers, referrals, tokenscope RPC_URL_11155111= RPC_REQUEST_RATE_LIMIT_11155111=500 @@ -102,6 +104,7 @@ RPC_REQUEST_RATE_LIMIT_534351=500 # === ENS Namespace: Holesky === # Ethereum Holesky (public testnet) # - required if the configured namespace is holesky +# - required by plugins: subgraph, reverse-resolvers, referrals RPC_URL_17000= RPC_REQUEST_RATE_LIMIT_17000=500 @@ -145,7 +148,7 @@ NAMESPACE=mainnet # - for subgraph-compatible indexing, the only valid configuration is `PLUGINS=subgraph`. # - for protocol acceleration of primary name lookups (reverse resolution), enable the # reverse-resolvers plugin. -PLUGINS=subgraph,basenames,lineanames,threedns,reverse-resolvers,referrals +PLUGINS=subgraph,basenames,lineanames,threedns,reverse-resolvers,referrals,tokenscope # ENSRainbow service URL # Required. This is the URL of the ENSRainbow server that ENSIndexer will use to heal diff --git a/docs/ensnode.io/src/content/docs/docs/usage/api.mdx b/docs/ensnode.io/src/content/docs/docs/usage/api.mdx index 31099ef20..2a7fa9c61 100644 --- a/docs/ensnode.io/src/content/docs/docs/usage/api.mdx +++ b/docs/ensnode.io/src/content/docs/docs/usage/api.mdx @@ -113,7 +113,8 @@ The response includes several important configuration categories: "lineanames", "threedns", "reverse-resolvers", - "referrals" + "referrals", + "tokenscope" ], "dependencyInfo": { "nodejs": "22.18.0", diff --git a/packages/ensnode-sdk/src/ensindexer/config/zod-schemas.test.ts b/packages/ensnode-sdk/src/ensindexer/config/zod-schemas.test.ts index c409e237b..3e75e1eb5 100644 --- a/packages/ensnode-sdk/src/ensindexer/config/zod-schemas.test.ts +++ b/packages/ensnode-sdk/src/ensindexer/config/zod-schemas.test.ts @@ -44,7 +44,7 @@ describe("ENSIndexer: Config", () => { ).toContain("Plugins cannot contain duplicate values"); expect(formatParseError(makePluginsListSchema().safeParse([]))).toContain( - "Plugins must be a list with at least one valid plugin name. Valid plugins are: subgraph, basenames, lineanames, threedns, reverse-resolvers, referrals", + "Plugins must be a list with at least one valid plugin name. Valid plugins are: subgraph, basenames, lineanames, threedns, reverse-resolvers, referrals, tokenscope", ); }); diff --git a/terraform/main.tf b/terraform/main.tf index 58bdd0ec1..8cec51e19 100644 --- a/terraform/main.tf +++ b/terraform/main.tf @@ -40,7 +40,7 @@ locals { instance_name = "alpha" subdomain_prefix = "alpha.${var.render_environment}" database_schema = "alphaSchema-${var.ensnode_version}" - plugins = "subgraph,basenames,lineanames,threedns,reverse-resolvers,referrals" + plugins = "subgraph,basenames,lineanames,threedns,reverse-resolvers,referrals,tokenscope" namespace = "mainnet" heal_reverse_addresses = "true" index_additional_resolver_records = "true" @@ -51,7 +51,7 @@ locals { instance_name = "alpha-sepolia" subdomain_prefix = "alpha-sepolia.${var.render_environment}" database_schema = "alphaSepoliaSchema-${var.ensnode_version}" - plugins = "subgraph,basenames,lineanames,reverse-resolvers,referrals" + plugins = "subgraph,basenames,lineanames,reverse-resolvers,referrals,tokenscope" namespace = "sepolia" heal_reverse_addresses = "true" index_additional_resolver_records = "true" From 4e3b81dcfa8a091338c9c75d9e79be714bf3e954 Mon Sep 17 00:00:00 2001 From: "lightwalker.eth" <126201998+lightwalker-eth@users.noreply.github.com> Date: Mon, 25 Aug 2025 21:20:35 +0300 Subject: [PATCH 29/61] Revert chainId import changes --- .../src/components/indexing-status/block-refs.tsx | 3 +-- .../src/components/indexing-status/indexing-timeline.tsx | 3 +-- apps/ensadmin/src/lib/namespace-utils.ts | 3 +-- .../lib/acceleration/known-ensip-19-reverse-resolvers.ts | 3 ++- .../lib/acceleration/known-offchain-lookup-resolver.ts | 4 ++-- .../api/lib/acceleration/known-onchain-static-resolver.ts | 3 ++- .../lib/acceleration/resolver-records-indexed-on-chain.ts | 4 ++-- apps/ensindexer/src/api/lib/find-resolver.ts | 4 ++-- apps/ensindexer/src/api/lib/get-records-from-index.ts | 8 ++++++-- apps/ensindexer/src/api/lib/public-client.ts | 2 +- .../lib/resolution/multichain-primary-name-resolution.ts | 8 ++------ apps/ensindexer/src/config/config.schema.ts | 4 ++-- apps/ensindexer/src/config/derived-params.ts | 3 +-- apps/ensindexer/src/config/serialized-types.ts | 3 +-- apps/ensindexer/src/config/types.ts | 4 ++-- .../src/indexing-status/ponder-metadata/chains.ts | 2 +- apps/ensindexer/src/lib/ponder-helpers.ts | 5 +++-- packages/ensnode-sdk/src/ens/coin-type.ts | 2 +- packages/ensnode-sdk/src/ensindexer/api/zod-schemas.ts | 3 +-- packages/ensnode-sdk/src/ensindexer/config/serialize.ts | 2 +- .../ensnode-sdk/src/ensindexer/config/serialized-types.ts | 3 +-- .../ensnode-sdk/src/ensindexer/indexing-status/helpers.ts | 3 +-- .../src/ensindexer/indexing-status/serialize.ts | 4 +--- .../ensnode-sdk/src/ensindexer/indexing-status/types.ts | 3 +-- .../src/ensindexer/indexing-status/zod-schemas.ts | 3 +-- packages/ensnode-sdk/src/resolution/types.ts | 2 +- packages/ensnode-sdk/src/shared/deserialize.ts | 3 +-- packages/ensnode-sdk/src/shared/serialize.ts | 3 +-- packages/ensnode-sdk/src/shared/types.ts | 5 +++++ 29 files changed, 48 insertions(+), 54 deletions(-) diff --git a/apps/ensadmin/src/components/indexing-status/block-refs.tsx b/apps/ensadmin/src/components/indexing-status/block-refs.tsx index 767657a1e..398d0d528 100644 --- a/apps/ensadmin/src/components/indexing-status/block-refs.tsx +++ b/apps/ensadmin/src/components/indexing-status/block-refs.tsx @@ -4,8 +4,7 @@ import { RelativeTime } from "@/components/datetime-utils"; import { getBlockExplorerUrlForBlock } from "@/lib/namespace-utils"; -import { ChainId } from "@ensnode/datasources"; -import { BlockRef } from "@ensnode/ensnode-sdk"; +import { BlockRef, ChainId } from "@ensnode/ensnode-sdk"; import { fromUnixTime } from "date-fns"; import { ExternalLink } from "lucide-react"; diff --git a/apps/ensadmin/src/components/indexing-status/indexing-timeline.tsx b/apps/ensadmin/src/components/indexing-status/indexing-timeline.tsx index a38ab2d1a..ff5674c56 100644 --- a/apps/ensadmin/src/components/indexing-status/indexing-timeline.tsx +++ b/apps/ensadmin/src/components/indexing-status/indexing-timeline.tsx @@ -4,8 +4,7 @@ import { ChainName } from "@/components/chains/ChainName"; import { cn } from "@/lib/utils"; -import { ChainId } from "@ensnode/datasources"; -import { ChainIndexingStatusIds } from "@ensnode/ensnode-sdk"; +import { ChainId, ChainIndexingStatusIds } from "@ensnode/ensnode-sdk"; import { intlFormat } from "date-fns"; import { BlockRefViewModel } from "@/components/indexing-status/block-refs"; diff --git a/apps/ensadmin/src/lib/namespace-utils.ts b/apps/ensadmin/src/lib/namespace-utils.ts index 1755eecaa..cefe1732a 100644 --- a/apps/ensadmin/src/lib/namespace-utils.ts +++ b/apps/ensadmin/src/lib/namespace-utils.ts @@ -1,11 +1,10 @@ import { - ChainId, DatasourceNames, ENSNamespaceId, ENSNamespaceIds, getDatasource, } from "@ensnode/datasources"; -import { Name } from "@ensnode/ensnode-sdk"; +import { ChainId, Name } from "@ensnode/ensnode-sdk"; import { Address } from "viem"; import { anvil, diff --git a/apps/ensindexer/src/api/lib/acceleration/known-ensip-19-reverse-resolvers.ts b/apps/ensindexer/src/api/lib/acceleration/known-ensip-19-reverse-resolvers.ts index 715f48ea7..c5f0c5648 100644 --- a/apps/ensindexer/src/api/lib/acceleration/known-ensip-19-reverse-resolvers.ts +++ b/apps/ensindexer/src/api/lib/acceleration/known-ensip-19-reverse-resolvers.ts @@ -1,5 +1,6 @@ import config from "@/config"; -import { ChainId, DatasourceNames, maybeGetDatasource } from "@ensnode/datasources"; +import { DatasourceNames, maybeGetDatasource } from "@ensnode/datasources"; +import { ChainId } from "@ensnode/ensnode-sdk"; import { Address } from "viem"; const rrRoot = maybeGetDatasource(config.namespace, DatasourceNames.ReverseResolverRoot); diff --git a/apps/ensindexer/src/api/lib/acceleration/known-offchain-lookup-resolver.ts b/apps/ensindexer/src/api/lib/acceleration/known-offchain-lookup-resolver.ts index 86e317ba2..5ecb45591 100644 --- a/apps/ensindexer/src/api/lib/acceleration/known-offchain-lookup-resolver.ts +++ b/apps/ensindexer/src/api/lib/acceleration/known-offchain-lookup-resolver.ts @@ -1,6 +1,6 @@ import config from "@/config"; -import { ChainId, DatasourceNames, maybeGetDatasource } from "@ensnode/datasources"; -import { PluginName } from "@ensnode/ensnode-sdk"; +import { DatasourceNames, maybeGetDatasource } from "@ensnode/datasources"; +import { ChainId, PluginName } from "@ensnode/ensnode-sdk"; import { Address, isAddressEqual } from "viem"; // NOTE: we know ensRoot is defined for all namespaces, so enforce that at runtime with ! diff --git a/apps/ensindexer/src/api/lib/acceleration/known-onchain-static-resolver.ts b/apps/ensindexer/src/api/lib/acceleration/known-onchain-static-resolver.ts index 3e3d29364..e13ae52a8 100644 --- a/apps/ensindexer/src/api/lib/acceleration/known-onchain-static-resolver.ts +++ b/apps/ensindexer/src/api/lib/acceleration/known-onchain-static-resolver.ts @@ -1,5 +1,6 @@ import config from "@/config"; -import { ChainId, DatasourceNames, maybeGetDatasource } from "@ensnode/datasources"; +import { DatasourceNames, maybeGetDatasource } from "@ensnode/datasources"; +import { ChainId } from "@ensnode/ensnode-sdk"; import { Address } from "viem"; const rrRoot = maybeGetDatasource(config.namespace, DatasourceNames.ReverseResolverRoot); diff --git a/apps/ensindexer/src/api/lib/acceleration/resolver-records-indexed-on-chain.ts b/apps/ensindexer/src/api/lib/acceleration/resolver-records-indexed-on-chain.ts index 49b5fc809..2c6f475da 100644 --- a/apps/ensindexer/src/api/lib/acceleration/resolver-records-indexed-on-chain.ts +++ b/apps/ensindexer/src/api/lib/acceleration/resolver-records-indexed-on-chain.ts @@ -1,6 +1,6 @@ import config from "@/config"; -import { ChainId, DatasourceNames, maybeGetDatasource } from "@ensnode/datasources"; -import { PluginName } from "@ensnode/ensnode-sdk"; +import { DatasourceNames, maybeGetDatasource } from "@ensnode/datasources"; +import { ChainId, PluginName } from "@ensnode/ensnode-sdk"; // NOTE: we know ensRoot is defined for all namespaces, so enforce that at runtime with ! const ensRoot = maybeGetDatasource(config.namespace, DatasourceNames.ENSRoot)!; diff --git a/apps/ensindexer/src/api/lib/find-resolver.ts b/apps/ensindexer/src/api/lib/find-resolver.ts index 38a0297d2..3a55b41cd 100644 --- a/apps/ensindexer/src/api/lib/find-resolver.ts +++ b/apps/ensindexer/src/api/lib/find-resolver.ts @@ -1,6 +1,6 @@ import { db } from "ponder:api"; -import { ChainId, DatasourceNames, getDatasource, getENSRootChainId } from "@ensnode/datasources"; -import { type Name, type Node, PluginName, getNameHierarchy } from "@ensnode/ensnode-sdk"; +import { DatasourceNames, getDatasource, getENSRootChainId } from "@ensnode/datasources"; +import { ChainId, type Name, type Node, PluginName, getNameHierarchy } from "@ensnode/ensnode-sdk"; import { SpanStatusCode, trace } from "@opentelemetry/api"; import { type Address, diff --git a/apps/ensindexer/src/api/lib/get-records-from-index.ts b/apps/ensindexer/src/api/lib/get-records-from-index.ts index 541ad92ab..bcb09e910 100644 --- a/apps/ensindexer/src/api/lib/get-records-from-index.ts +++ b/apps/ensindexer/src/api/lib/get-records-from-index.ts @@ -3,8 +3,12 @@ import { onchainStaticResolverImplementsDefaultAddress } from "@/api/lib/acceler import type { IndexedResolverRecords } from "@/api/lib/make-records-response"; import { withSpanAsync } from "@/lib/auto-span"; import { makeResolverId } from "@/lib/ids"; -import { ChainId } from "@ensnode/datasources"; -import { DEFAULT_EVM_COIN_TYPE, Node, ResolverRecordsSelection } from "@ensnode/ensnode-sdk"; +import { + ChainId, + DEFAULT_EVM_COIN_TYPE, + Node, + ResolverRecordsSelection, +} from "@ensnode/ensnode-sdk"; import { trace } from "@opentelemetry/api"; import { Address } from "viem"; diff --git a/apps/ensindexer/src/api/lib/public-client.ts b/apps/ensindexer/src/api/lib/public-client.ts index 64314b13c..5900f1d49 100644 --- a/apps/ensindexer/src/api/lib/public-client.ts +++ b/apps/ensindexer/src/api/lib/public-client.ts @@ -1,5 +1,5 @@ import config from "@/config"; -import { ChainId } from "@ensnode/datasources"; +import { ChainId } from "@ensnode/ensnode-sdk"; import { http, createPublicClient } from "viem"; export function getPublicClient(chainId: ChainId) { diff --git a/apps/ensindexer/src/api/lib/resolution/multichain-primary-name-resolution.ts b/apps/ensindexer/src/api/lib/resolution/multichain-primary-name-resolution.ts index 0c4a24837..93144066f 100644 --- a/apps/ensindexer/src/api/lib/resolution/multichain-primary-name-resolution.ts +++ b/apps/ensindexer/src/api/lib/resolution/multichain-primary-name-resolution.ts @@ -1,4 +1,5 @@ import { + type ChainId, type MultichainPrimaryNameResolutionArgs, type MultichainPrimaryNameResolutionResult, uniq, @@ -8,12 +9,7 @@ import { trace } from "@opentelemetry/api"; import { resolveReverse } from "@/api/lib/resolution/reverse-resolution"; import config from "@/config"; import { withActiveSpanAsync } from "@/lib/auto-span"; -import { - type ChainId, - DatasourceNames, - getDatasource, - maybeGetDatasource, -} from "@ensnode/datasources"; +import { DatasourceNames, getDatasource, maybeGetDatasource } from "@ensnode/datasources"; const tracer = trace.getTracer("multichain-primary-name-resolution"); diff --git a/apps/ensindexer/src/config/config.schema.ts b/apps/ensindexer/src/config/config.schema.ts index 0483aa0c8..0470dc17b 100644 --- a/apps/ensindexer/src/config/config.schema.ts +++ b/apps/ensindexer/src/config/config.schema.ts @@ -1,8 +1,8 @@ import { parse as parseConnectionString } from "pg-connection-string"; import { prettifyError, z } from "zod/v4"; -import { type ChainId, ENSNamespaceIds } from "@ensnode/datasources"; -import { PluginName, deserializeChainId, uniq } from "@ensnode/ensnode-sdk"; +import { ENSNamespaceIds } from "@ensnode/datasources"; +import { type ChainId, PluginName, deserializeChainId, uniq } from "@ensnode/ensnode-sdk"; import { makeUrlSchema } from "@ensnode/ensnode-sdk/internal"; import { diff --git a/apps/ensindexer/src/config/derived-params.ts b/apps/ensindexer/src/config/derived-params.ts index 0827cdc63..91a6a574f 100644 --- a/apps/ensindexer/src/config/derived-params.ts +++ b/apps/ensindexer/src/config/derived-params.ts @@ -1,8 +1,7 @@ import type { ENSIndexerConfig } from "@/config/types"; import { getENSNamespaceAsFullyDefinedAtCompileTime } from "@/lib/plugin-helpers"; import { getPlugin } from "@/plugins"; -import { ChainId } from "@ensnode/datasources"; -import { isSubgraphCompatible } from "@ensnode/ensnode-sdk"; +import { ChainId, isSubgraphCompatible } from "@ensnode/ensnode-sdk"; /** * Derive `indexedChainIds` configuration parameter and include it in diff --git a/apps/ensindexer/src/config/serialized-types.ts b/apps/ensindexer/src/config/serialized-types.ts index 904e5a99f..35cebb3bc 100644 --- a/apps/ensindexer/src/config/serialized-types.ts +++ b/apps/ensindexer/src/config/serialized-types.ts @@ -1,5 +1,4 @@ -import { ChainId } from "@ensnode/datasources"; -import type { ChainIdString, UrlString } from "@ensnode/ensnode-sdk"; +import type { ChainId, ChainIdString, UrlString } from "@ensnode/ensnode-sdk"; import type { ENSIndexerConfig, RpcConfig } from "./types"; /** diff --git a/apps/ensindexer/src/config/types.ts b/apps/ensindexer/src/config/types.ts index d75acd3a1..09fee8a30 100644 --- a/apps/ensindexer/src/config/types.ts +++ b/apps/ensindexer/src/config/types.ts @@ -1,5 +1,5 @@ -import type { ChainId, ENSNamespaceId, ENSNamespaceIds } from "@ensnode/datasources"; -import type { Blockrange, ChainIdString, PluginName } from "@ensnode/ensnode-sdk"; +import type { ENSNamespaceId, ENSNamespaceIds } from "@ensnode/datasources"; +import type { Blockrange, ChainId, ChainIdString, PluginName } from "@ensnode/ensnode-sdk"; /** * Configuration for a single RPC used by ENSIndexer. diff --git a/apps/ensindexer/src/indexing-status/ponder-metadata/chains.ts b/apps/ensindexer/src/indexing-status/ponder-metadata/chains.ts index 203dc7c8a..3a67b44ed 100644 --- a/apps/ensindexer/src/indexing-status/ponder-metadata/chains.ts +++ b/apps/ensindexer/src/indexing-status/ponder-metadata/chains.ts @@ -11,9 +11,9 @@ * all come together to form a single view about a chain's indexing status. */ -import { ChainId } from "@ensnode/datasources"; import { type BlockRef, + type ChainId, type ChainIndexingBackfillStatus, type ChainIndexingCompletedStatus, type ChainIndexingFollowingStatus, diff --git a/apps/ensindexer/src/lib/ponder-helpers.ts b/apps/ensindexer/src/lib/ponder-helpers.ts index b81f38a6f..4439c1e2c 100644 --- a/apps/ensindexer/src/lib/ponder-helpers.ts +++ b/apps/ensindexer/src/lib/ponder-helpers.ts @@ -10,11 +10,12 @@ import type { ChainConfig } from "ponder"; import { Address, PublicClient } from "viem"; import * as z from "zod/v4"; -import { ChainId, ContractConfig } from "@ensnode/datasources"; +import { ContractConfig } from "@ensnode/datasources"; +import { EnsRainbowApiClient } from "@ensnode/ensrainbow-sdk"; import type { BlockInfo, PonderStatus } from "@ensnode/ponder-metadata"; import { ENSIndexerConfig } from "@/config/types"; -import type { Blockrange } from "@ensnode/ensnode-sdk"; +import type { Blockrange, ChainId } from "@ensnode/ensnode-sdk"; export type EventWithArgs = {}> = Omit & { args: ARGS; diff --git a/packages/ensnode-sdk/src/ens/coin-type.ts b/packages/ensnode-sdk/src/ens/coin-type.ts index 7443bc943..3b97ad67b 100644 --- a/packages/ensnode-sdk/src/ens/coin-type.ts +++ b/packages/ensnode-sdk/src/ens/coin-type.ts @@ -3,7 +3,7 @@ import { coinTypeToEvmChainId as _coinTypeToEvmChainId, evmChainIdToCoinType as _evmChainIdToCoinType, } from "@ensdomains/address-encoder/utils"; -import { ChainId } from "@ensnode/datasources"; +import { ChainId } from "../shared"; // re-export CoinType and EvmCoinType from @ensdomains/address-encoder // so consumers don't need it as a dependency diff --git a/packages/ensnode-sdk/src/ensindexer/api/zod-schemas.ts b/packages/ensnode-sdk/src/ensindexer/api/zod-schemas.ts index 074c8722b..67d3c77bd 100644 --- a/packages/ensnode-sdk/src/ensindexer/api/zod-schemas.ts +++ b/packages/ensnode-sdk/src/ensindexer/api/zod-schemas.ts @@ -1,11 +1,10 @@ import { Address, getAddress } from "viem"; import { z } from "zod/v4"; -import { ChainId } from "@ensnode/datasources"; import { CoinType, DEFAULT_EVM_CHAIN_ID } from "../../ens/coin-type"; import { Name } from "../../ens/types"; import { ResolverRecordsSelection, isSelectionEmpty } from "../../resolution"; -import { isNormalized } from "../../shared"; +import { ChainId, isNormalized } from "../../shared"; import { makeDurationSchema } from "../../shared/zod-schemas"; const toName = (val: string) => val as Name; diff --git a/packages/ensnode-sdk/src/ensindexer/config/serialize.ts b/packages/ensnode-sdk/src/ensindexer/config/serialize.ts index 15f5e28f1..be01bd800 100644 --- a/packages/ensnode-sdk/src/ensindexer/config/serialize.ts +++ b/packages/ensnode-sdk/src/ensindexer/config/serialize.ts @@ -1,4 +1,4 @@ -import { ChainId } from "@ensnode/datasources"; +import { ChainId } from "../../shared"; import { serializeUrl } from "../../shared/serialize"; import { SerializedENSIndexerPublicConfig, SerializedIndexedChainIds } from "./serialized-types"; import { ENSIndexerPublicConfig } from "./types"; diff --git a/packages/ensnode-sdk/src/ensindexer/config/serialized-types.ts b/packages/ensnode-sdk/src/ensindexer/config/serialized-types.ts index 7fd7337f9..2882c2443 100644 --- a/packages/ensnode-sdk/src/ensindexer/config/serialized-types.ts +++ b/packages/ensnode-sdk/src/ensindexer/config/serialized-types.ts @@ -1,5 +1,4 @@ -import type { ChainId } from "@ensnode/datasources"; -import type { UrlString } from "../../shared"; +import type { ChainId, UrlString } from "../../shared"; import type { ENSIndexerPublicConfig } from "./types"; export type SerializedIndexedChainIds = Array; diff --git a/packages/ensnode-sdk/src/ensindexer/indexing-status/helpers.ts b/packages/ensnode-sdk/src/ensindexer/indexing-status/helpers.ts index e4b577436..757849728 100644 --- a/packages/ensnode-sdk/src/ensindexer/indexing-status/helpers.ts +++ b/packages/ensnode-sdk/src/ensindexer/indexing-status/helpers.ts @@ -1,5 +1,4 @@ -import { ChainId } from "@ensnode/datasources"; -import { BlockRef, Duration, UnixTimestamp } from "../../shared"; +import { BlockRef, ChainId, Duration, UnixTimestamp } from "../../shared"; import { ChainIndexingActiveStatus, ChainIndexingCompletedStatus, diff --git a/packages/ensnode-sdk/src/ensindexer/indexing-status/serialize.ts b/packages/ensnode-sdk/src/ensindexer/indexing-status/serialize.ts index e7e37fe2e..93445ade5 100644 --- a/packages/ensnode-sdk/src/ensindexer/indexing-status/serialize.ts +++ b/packages/ensnode-sdk/src/ensindexer/indexing-status/serialize.ts @@ -1,6 +1,4 @@ -import { ChainId } from "@ensnode/datasources"; -import { ChainIdString, serializeChainId } from "../../shared"; - +import { ChainId, ChainIdString, serializeChainId } from "../../shared"; import { SerializedENSIndexerOverallIndexingBackfillStatus, SerializedENSIndexerOverallIndexingCompletedStatus, diff --git a/packages/ensnode-sdk/src/ensindexer/indexing-status/types.ts b/packages/ensnode-sdk/src/ensindexer/indexing-status/types.ts index 5042f6bb4..fa7c21ac4 100644 --- a/packages/ensnode-sdk/src/ensindexer/indexing-status/types.ts +++ b/packages/ensnode-sdk/src/ensindexer/indexing-status/types.ts @@ -1,5 +1,4 @@ -import type { ChainId } from "@ensnode/datasources"; -import type { BlockRef, Duration, UnixTimestamp } from "../../shared"; +import type { BlockRef, ChainId, Duration, UnixTimestamp } from "../../shared"; export const ChainIndexingStatusIds = { Unstarted: "unstarted", diff --git a/packages/ensnode-sdk/src/ensindexer/indexing-status/zod-schemas.ts b/packages/ensnode-sdk/src/ensindexer/indexing-status/zod-schemas.ts index a73e6060c..27c039cc1 100644 --- a/packages/ensnode-sdk/src/ensindexer/indexing-status/zod-schemas.ts +++ b/packages/ensnode-sdk/src/ensindexer/indexing-status/zod-schemas.ts @@ -6,9 +6,8 @@ * The only way to share Zod schemas is to re-export them from * `./src/internal.ts` file. */ -import { ChainId } from "@ensnode/datasources"; import z from "zod/v4"; -import { deserializeChainId } from "../../shared"; +import { ChainId, deserializeChainId } from "../../shared"; import * as blockRef from "../../shared/block-ref"; import { makeBlockRefSchema, diff --git a/packages/ensnode-sdk/src/resolution/types.ts b/packages/ensnode-sdk/src/resolution/types.ts index d338c7123..36aae5ec4 100644 --- a/packages/ensnode-sdk/src/resolution/types.ts +++ b/packages/ensnode-sdk/src/resolution/types.ts @@ -1,7 +1,7 @@ import { Address } from "viem"; -import { ChainId } from "@ensnode/datasources"; import type { Name } from "../ens"; +import { ChainId } from "../shared"; import { ResolverRecordsResponse } from "./resolver-records-response"; import type { ResolverRecordsSelection } from "./resolver-records-selection"; diff --git a/packages/ensnode-sdk/src/shared/deserialize.ts b/packages/ensnode-sdk/src/shared/deserialize.ts index e443040a3..c628f3a73 100644 --- a/packages/ensnode-sdk/src/shared/deserialize.ts +++ b/packages/ensnode-sdk/src/shared/deserialize.ts @@ -1,7 +1,6 @@ -import { ChainId } from "@ensnode/datasources"; import { prettifyError } from "zod/v4"; import type { ChainIdString, UrlString } from "./serialized-types"; -import type { BlockNumber, BlockRef, Blockrange, Datetime, Duration } from "./types"; +import type { BlockNumber, BlockRef, Blockrange, ChainId, Datetime, Duration } from "./types"; import { makeBlockNumberSchema, makeBlockRefSchema, diff --git a/packages/ensnode-sdk/src/shared/serialize.ts b/packages/ensnode-sdk/src/shared/serialize.ts index d2b23ef4f..80faf6ba9 100644 --- a/packages/ensnode-sdk/src/shared/serialize.ts +++ b/packages/ensnode-sdk/src/shared/serialize.ts @@ -1,6 +1,5 @@ -import { ChainId } from "@ensnode/datasources"; import type { ChainIdString, DatetimeISO8601, UrlString } from "./serialized-types"; -import type { Datetime } from "./types"; +import type { ChainId, Datetime } from "./types"; /** * Serializes a {@link ChainId} value into its string representation. diff --git a/packages/ensnode-sdk/src/shared/types.ts b/packages/ensnode-sdk/src/shared/types.ts index 7705a73e2..e7c13b8e4 100644 --- a/packages/ensnode-sdk/src/shared/types.ts +++ b/packages/ensnode-sdk/src/shared/types.ts @@ -1,3 +1,8 @@ +import { ChainId } from "@ensnode/datasources"; + +// re-export ChainId for backwards compatibility +export { type ChainId }; + /** * Block Number * From ce7d125d6ea568800f3b85d66563acc0ecf0160c Mon Sep 17 00:00:00 2001 From: "lightwalker.eth" <126201998+lightwalker-eth@users.noreply.github.com> Date: Mon, 25 Aug 2025 21:42:14 +0300 Subject: [PATCH 30/61] Document division of responsibilities --- apps/ensindexer/src/handlers/Seaport.ts | 5 +++++ apps/ensindexer/src/lib/tokenscope-helpers.ts | 5 +++++ 2 files changed, 10 insertions(+) diff --git a/apps/ensindexer/src/handlers/Seaport.ts b/apps/ensindexer/src/handlers/Seaport.ts index d751e8877..7d2159a5b 100644 --- a/apps/ensindexer/src/handlers/Seaport.ts +++ b/apps/ensindexer/src/handlers/Seaport.ts @@ -19,6 +19,11 @@ import { import { ChainId, ENSNamespaceId } from "@ensnode/datasources"; import { Address, Hex } from "viem"; +/** + * The file has the responsibility for logic that maps from Seaport-specific data models + * into our more generic Tokenscope data models as found in the `@/lib/tokenscope-helpers` file. + */ + type SeaportOfferItem = { /** * The type of item in the offer. diff --git a/apps/ensindexer/src/lib/tokenscope-helpers.ts b/apps/ensindexer/src/lib/tokenscope-helpers.ts index 030ea420c..d34bf573f 100644 --- a/apps/ensindexer/src/lib/tokenscope-helpers.ts +++ b/apps/ensindexer/src/lib/tokenscope-helpers.ts @@ -32,6 +32,11 @@ import { sepolia, } from "viem/chains"; +/** + * This file has the responsibility for "core" TokenScope data models and logic that are generic and + * are not specifically related only to a single marketplace. Ex: Seaport. + */ + export const TokenTypes = { ERC721: "ERC721", ERC1155: "ERC1155", From 2835c351a86d4ed3aaee41dbf8b3816d0ebb9bb6 Mon Sep 17 00:00:00 2001 From: "lightwalker.eth" <126201998+lightwalker-eth@users.noreply.github.com> Date: Mon, 25 Aug 2025 21:50:43 +0300 Subject: [PATCH 31/61] Add docs --- apps/ensindexer/src/handlers/Seaport.ts | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/apps/ensindexer/src/handlers/Seaport.ts b/apps/ensindexer/src/handlers/Seaport.ts index 7d2159a5b..ff0d177d2 100644 --- a/apps/ensindexer/src/handlers/Seaport.ts +++ b/apps/ensindexer/src/handlers/Seaport.ts @@ -21,7 +21,16 @@ import { Address, Hex } from "viem"; /** * The file has the responsibility for logic that maps from Seaport-specific data models - * into our more generic Tokenscope data models as found in the `@/lib/tokenscope-helpers` file. + * into our more generic TokenScope data models as found in the `@/lib/tokenscope-helpers` file. + * + * Seaport's data model supports complexity that has more negatives than benefits. TokenScope aims + * to deliver a more simple data model for developers to build ENS apps with. This simplified data + * model is anticipated to still support the vast majority of real-world use cases. + * + * In this file we examine each indexed Seaport event to determine if it fits within the TokenScope + * data model. If it does, we extract the relevant data and map it into the TokenScope data model. + * + * If it does not, we ignore the event. */ type SeaportOfferItem = { From aa29450fd12447be008509ca3b1c8a2ce819af49 Mon Sep 17 00:00:00 2001 From: "lightwalker.eth" <126201998+lightwalker-eth@users.noreply.github.com> Date: Mon, 25 Aug 2025 21:54:07 +0300 Subject: [PATCH 32/61] Refine docs --- apps/ensindexer/src/handlers/Seaport.ts | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/apps/ensindexer/src/handlers/Seaport.ts b/apps/ensindexer/src/handlers/Seaport.ts index ff0d177d2..3b92bc1f8 100644 --- a/apps/ensindexer/src/handlers/Seaport.ts +++ b/apps/ensindexer/src/handlers/Seaport.ts @@ -251,6 +251,15 @@ const getSupportedPayment = ( interface SeaportItemExtractions { nfts: SupportedNFT[]; + + /** + * Seaport supports multiple payments in a single order. + * + * Example cases include: + * - Payments are being made in multiple currencies. + * - Multiple payments in the same currency, but where payment is for marketplace fees while + * other payments are for the seller. + */ payments: SupportedPayment[]; } @@ -297,6 +306,7 @@ const consolidateSupportedPayments = (payments: SupportedPayment[]): SupportedPa return null; // Either no payment or multiple payments in mixed currencies } + // consolidate multiple payments in the same currency into one. const totalAmount = payments.reduce((total, payment) => total + payment.price.amount, 0n); return { From b8761f1f680af08f2ad54e3d1301368898390b41 Mon Sep 17 00:00:00 2001 From: "lightwalker.eth" <126201998+lightwalker-eth@users.noreply.github.com> Date: Mon, 25 Aug 2025 21:54:28 +0300 Subject: [PATCH 33/61] lint --- apps/ensindexer/src/handlers/Seaport.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/apps/ensindexer/src/handlers/Seaport.ts b/apps/ensindexer/src/handlers/Seaport.ts index 3b92bc1f8..6afffec9d 100644 --- a/apps/ensindexer/src/handlers/Seaport.ts +++ b/apps/ensindexer/src/handlers/Seaport.ts @@ -22,14 +22,14 @@ import { Address, Hex } from "viem"; /** * The file has the responsibility for logic that maps from Seaport-specific data models * into our more generic TokenScope data models as found in the `@/lib/tokenscope-helpers` file. - * + * * Seaport's data model supports complexity that has more negatives than benefits. TokenScope aims * to deliver a more simple data model for developers to build ENS apps with. This simplified data * model is anticipated to still support the vast majority of real-world use cases. - * + * * In this file we examine each indexed Seaport event to determine if it fits within the TokenScope * data model. If it does, we extract the relevant data and map it into the TokenScope data model. - * + * * If it does not, we ignore the event. */ @@ -254,7 +254,7 @@ interface SeaportItemExtractions { /** * Seaport supports multiple payments in a single order. - * + * * Example cases include: * - Payments are being made in multiple currencies. * - Multiple payments in the same currency, but where payment is for marketplace fees while From b8d065acff23fdd9bab969dc43c9aa198f0df25c Mon Sep 17 00:00:00 2001 From: shrugs Date: Mon, 25 Aug 2025 14:30:21 -0500 Subject: [PATCH 34/61] add tokenscope plugin to alpha docs list --- .../src/content/docs/docs/usage/hosted-ensnode-instances.mdx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/ensnode.io/src/content/docs/docs/usage/hosted-ensnode-instances.mdx b/docs/ensnode.io/src/content/docs/docs/usage/hosted-ensnode-instances.mdx index 3847ff472..0d752fcbc 100644 --- a/docs/ensnode.io/src/content/docs/docs/usage/hosted-ensnode-instances.mdx +++ b/docs/ensnode.io/src/content/docs/docs/usage/hosted-ensnode-instances.mdx @@ -30,7 +30,7 @@ These ENSNode instances focus on pushing the boundaries of what's possible with instanceURL="https://api.alpha.ensnode.io" connectWithENSAdminURL="https://admin.ensnode.io/connect?ensnode=https%3A%2F%2Fapi.alpha.ensnode.io" namespace="mainnet" - plugins="subgraph, basenames, lineanames, threedns, reverse-resolvers, referrals" + plugins="subgraph, basenames, lineanames, threedns, reverse-resolvers, referrals, tokenscope" purpose="Demonstration of ENSNode's multichain ENS indexing capability for the ENS namespace rooted on Mainnet. Provides a superset of the data indexed by the ENS Subgraph. Indexes additional Resolver record values in preparation for ENS Protocol Acceleration." /> @@ -39,7 +39,7 @@ These ENSNode instances focus on pushing the boundaries of what's possible with instanceURL="https://api.alpha-sepolia.ensnode.io" connectWithENSAdminURL="https://admin.ensnode.io/connect?ensnode=https%3A%2F%2Fapi.alpha-sepolia.ensnode.io" namespace="sepolia" - plugins="subgraph, basenames, lineanames, reverse-resolvers, referrals" + plugins="subgraph, basenames, lineanames, reverse-resolvers, referrals, tokenscope" purpose="Demonstration of ENSNode's multichain ENS indexing capability for the ENS namespace rooted on Sepolia. Provides a superset of the data indexed by the ENS Subgraph. Indexes additional Resolver record values in preparation for ENS Protocol Acceleration." /> From c7da331bfc9051348243a45ad00d5c634edecda2 Mon Sep 17 00:00:00 2001 From: shrugs Date: Mon, 25 Aug 2025 16:04:01 -0500 Subject: [PATCH 35/61] refactor: move handler and lib logics to correct place --- apps/ensindexer/package.json | 1 - apps/ensindexer/src/lib/currencies.ts | 212 +++++++ apps/ensindexer/src/lib/tokenscope-helpers.ts | 539 ------------------ apps/ensindexer/src/lib/tokenscope/sales.ts | 42 ++ .../src/lib/tokenscope/seaport-types.ts | 136 +++++ .../Seaport.ts => lib/tokenscope/seaport.ts} | 190 +----- .../src/lib/tokenscope/token-issuers.ts | 221 +++++++ apps/ensindexer/src/lib/tokenscope/tokens.ts | 28 + .../plugins/tokenscope/handlers/Seaport.ts | 42 +- .../ensnode-schema/src/tokenscope.schema.ts | 5 +- 10 files changed, 705 insertions(+), 711 deletions(-) create mode 100644 apps/ensindexer/src/lib/currencies.ts delete mode 100644 apps/ensindexer/src/lib/tokenscope-helpers.ts create mode 100644 apps/ensindexer/src/lib/tokenscope/sales.ts create mode 100644 apps/ensindexer/src/lib/tokenscope/seaport-types.ts rename apps/ensindexer/src/{handlers/Seaport.ts => lib/tokenscope/seaport.ts} (57%) create mode 100644 apps/ensindexer/src/lib/tokenscope/token-issuers.ts create mode 100644 apps/ensindexer/src/lib/tokenscope/tokens.ts diff --git a/apps/ensindexer/package.json b/apps/ensindexer/package.json index 9658b6ab6..88f229b3b 100644 --- a/apps/ensindexer/package.json +++ b/apps/ensindexer/package.json @@ -30,7 +30,6 @@ "@ensnode/ensrainbow-sdk": "workspace:*", "@ensnode/ponder-metadata": "workspace:*", "@ensnode/ponder-subgraph": "workspace:*", - "@opensea/seaport-js": "^4.0.5", "@hono/otel": "^0.2.2", "@hono/zod-validator": "^0.7.2", "@opentelemetry/api": "^1.9.0", diff --git a/apps/ensindexer/src/lib/currencies.ts b/apps/ensindexer/src/lib/currencies.ts new file mode 100644 index 000000000..b2c049cae --- /dev/null +++ b/apps/ensindexer/src/lib/currencies.ts @@ -0,0 +1,212 @@ +import { + base, + baseSepolia, + holesky, + linea, + lineaSepolia, + mainnet, + optimism, + sepolia, +} from "viem/chains"; + +import { + ChainAddress, + ChainId, + ENSNamespaceId, + getChainIdsForNamespace, + isChainAddressEqual, +} from "@ensnode/datasources"; +import { Address, zeroAddress } from "viem"; + +/** + * Identifiers for supported currencies. + * + * TODO: Add support for WETH + */ +export const CurrencyIds = { + ETH: "ETH", + USDC: "USDC", + DAI: "DAI", +} as const; + +export type CurrencyId = (typeof CurrencyIds)[keyof typeof CurrencyIds]; + +export interface Price { + currency: CurrencyId; + + /** + * The amount of the currency in the smallest unit of the currency. (see + * decimals of the CurrencyConfig for the currency). + * + * Guaranteed to be non-negative. + */ + amount: bigint; +} + +export interface CurrencyConfig { + id: CurrencyId; + name: string; + decimals: number; +} + +const currencyConfigs: Record = { + [CurrencyIds.ETH]: { + id: CurrencyIds.ETH, + name: "Ethereum", + decimals: 18, + }, + [CurrencyIds.USDC]: { + id: CurrencyIds.USDC, + name: "USDC", + decimals: 6, + }, + [CurrencyIds.DAI]: { + id: CurrencyIds.DAI, + name: "Dai Stablecoin", + decimals: 18, + }, +}; + +export const getCurrencyConfig = (currencyId: CurrencyId): CurrencyConfig => { + return currencyConfigs[currencyId]; +}; + +// NOTE: this mapping currently only considers the subset of chains where we have +// supported token issuing contracts. +const knownCurrencyContracts: Record> = { + /** mainnet namespace */ + [mainnet.id]: { + [CurrencyIds.ETH]: zeroAddress, + [CurrencyIds.USDC]: "0xA0b86a33E6417c5Dd4Baf8C54e5de49E293E9169", + [CurrencyIds.DAI]: "0x6B175474E89094C44Da98b954EedeAC495271d0F", + }, + [base.id]: { + [CurrencyIds.ETH]: zeroAddress, + [CurrencyIds.USDC]: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", + [CurrencyIds.DAI]: "0x50c5725949A6F0c72E6C4a641F24049A917DB0Cb", + }, + [optimism.id]: { + [CurrencyIds.ETH]: zeroAddress, + [CurrencyIds.USDC]: "0x0b2C639c533813f4Aa9D7837CAf62653d097Ff85", + [CurrencyIds.DAI]: "0xDA10009cBd5D07dd0CeCc66161FC93D7c9000da1", + }, + [linea.id]: { + [CurrencyIds.ETH]: zeroAddress, + [CurrencyIds.USDC]: "0x176211869cA2b568f2A7D4EE941E073a821EE1ff", + [CurrencyIds.DAI]: "0x4AF15ec2A0BD43Db75dd04E62FAA3B8EF36b00d5", + }, + /** sepolia namespace */ + [sepolia.id]: { + [CurrencyIds.ETH]: zeroAddress, + [CurrencyIds.USDC]: "0x1c7D4B196Cb0C7B01d743Fbc6116a902379C7238", + [CurrencyIds.DAI]: "0x3e622317f8C93f7328350cF0B56d9eD4C620C5d6", + }, + [baseSepolia.id]: { + [CurrencyIds.ETH]: zeroAddress, + [CurrencyIds.USDC]: "0x036CbD53842c5426634e7929541eC2318f3dCF7e", + [CurrencyIds.DAI]: "0x7368C6C68a4b2b68F90DB2e8F5E3b8E1E5e4F5c7", + }, + [lineaSepolia.id]: { + [CurrencyIds.ETH]: zeroAddress, + [CurrencyIds.USDC]: "0x176211869cA2b568f2A7D4EE941E073a821EE1ff", + [CurrencyIds.DAI]: "0x4AF15ec2A0BD43Db75dd04E62FAA3B8EF36b00d5", + }, + [holesky.id]: { + [CurrencyIds.ETH]: zeroAddress, + [CurrencyIds.USDC]: "0x1c7D4B196Cb0C7B01d743Fbc6116a902379C7238", + [CurrencyIds.DAI]: "0x3e622317f8C93f7328350cF0B56d9eD4C620C5d6", + }, +} as const; + +/** + * Gets the supported currency contracts for a given chain. + * + * @param chainId - The chain ID to get supported currency contracts for + * @returns a record of currency ids to ChainAddresses for the given chain + */ +export const getSupportedCurrencyContractsForChain = ( + chainId: ChainId, +): Record => { + let result = {} as Record; + + const knownCurrencyContractsForChain = knownCurrencyContracts[chainId]; + if (!knownCurrencyContractsForChain) { + return result; + } + + for (const [currencyId, address] of Object.entries(knownCurrencyContractsForChain)) { + result[currencyId as CurrencyId] = { + address, + chainId, + } as ChainAddress; + } + + return result; +}; + +/** + * Gets the supported currency contracts for a given namespace. + * + * @param namespaceId - The ENSNamespace identifier (e.g. 'mainnet', 'sepolia', 'holesky', + * 'ens-test-env') + * @returns a record of currency ids to ChainAddresses for the given namespace + */ +export const getSupportedCurrencyContractsForNamespace = ( + namespaceId: ENSNamespaceId, +): Record => { + let result = {} as Record; + const chainIds = getChainIdsForNamespace(namespaceId); + for (const chainId of chainIds) { + const supportedCurrencyContractsForChain = getSupportedCurrencyContractsForChain(chainId); + result = { ...result, ...supportedCurrencyContractsForChain }; + } + + return result; +}; + +/** + * Identifies if the provided ChainAddress is a supported currency contract in the + * specified namespace. + * + * @param namespaceId - The ENSNamespace identifier (e.g. 'mainnet', 'sepolia', 'holesky', + * 'ens-test-env') + * @param contract - The ChainAddress of the contract to check + * @returns a boolean indicating if the provided ChainAddress is a supported currency + * contract in the specified namespace + */ +export const isSupportedCurrencyContract = ( + namespaceId: ENSNamespaceId, + contract: ChainAddress, +): boolean => { + const supportedCurrencyContracts = getSupportedCurrencyContractsForNamespace(namespaceId); + return Object.values(supportedCurrencyContracts).some((supportedCurrencyContract) => + isChainAddressEqual(supportedCurrencyContract, contract), + ); +}; + +/** + * Gets the currency id for the given contract in the specified namespace. + * + * @param namespaceId - The ENSNamespace identifier (e.g. 'mainnet', 'sepolia', 'holesky', + * 'ens-test-env') + * @param contract - The ChainAddress of the contract to get the currency id for + * @returns the currency id for the given contract in the specified namespace, or + * null if the contract is not a supported currency contract in the + * specified namespace + */ +export const getCurrencyIdForContract = ( + namespaceId: ENSNamespaceId, + contract: ChainAddress, +): CurrencyId | null => { + const supportedCurrencyContracts = getSupportedCurrencyContractsForNamespace(namespaceId); + + for (const [currencyId, supportedCurrencyContract] of Object.entries( + supportedCurrencyContracts, + )) { + if (isChainAddressEqual(supportedCurrencyContract, contract)) { + return currencyId as CurrencyId; + } + } + + return null; +}; diff --git a/apps/ensindexer/src/lib/tokenscope-helpers.ts b/apps/ensindexer/src/lib/tokenscope-helpers.ts deleted file mode 100644 index d34bf573f..000000000 --- a/apps/ensindexer/src/lib/tokenscope-helpers.ts +++ /dev/null @@ -1,539 +0,0 @@ -import { Context } from "ponder:registry"; -import schema from "ponder:schema"; -import { upsertAccount } from "@/lib/db-helpers"; -import { - ChainAddress, - ChainId, - DatasourceNames, - ENSNamespaceId, - getChainIdsForNamespace, - isChainAddressEqual, - maybeGetDatasourceContractChainAddress, -} from "@ensnode/datasources"; -import { - BASENAMES_NODE, - ETH_NODE, - LINEANAMES_NODE, - LabelHash, - type Node, - UnixTimestamp, - makeSubdomainNode, - uint256ToHex32, -} from "@ensnode/ensnode-sdk"; -import { Address, Hex, zeroAddress } from "viem"; -import { - base, - baseSepolia, - holesky, - linea, - lineaSepolia, - mainnet, - optimism, - sepolia, -} from "viem/chains"; - -/** - * This file has the responsibility for "core" TokenScope data models and logic that are generic and - * are not specifically related only to a single marketplace. Ex: Seaport. - */ - -export const TokenTypes = { - ERC721: "ERC721", - ERC1155: "ERC1155", -} as const; - -export type TokenType = (typeof TokenTypes)[keyof typeof TokenTypes]; - -/** - * A uint256 value that identifies a specific token within a NFT contract. - */ -export type TokenId = bigint; - -/** - * A contract that issues tokenized ENS names. - */ -export interface TokenIssuer { - /** - * The ChainAddress of the token issuer contract. - */ - contract: ChainAddress; - - /** - * Applies the token issuer contract's logic for converting from the token id - * representation of a domain to the domain id (Node) representation of a domain. - */ - getDomainId: (tokenId: TokenId) => Node; -} - -/** - * Converts the tokenId from an ENS name token-issuing contract to a Node - * for the case that the contract generates each tokenId using namehash of - * the full name. - * - * @param tokenId - The tokenId to convert - * @returns The Node of the tokenId - */ -export const nameHashGeneratedTokenIdToNode = (tokenId: TokenId): Node => { - return uint256ToHex32(tokenId); -}; - -/** - * Converts the tokenId from an ENS name token-issuing contract to a Node - * for the case that the contract generates each tokenId using labelhash of - * the direct subname of the parent node. - * - * @param tokenId - The tokenId to convert - * @param parentNode - the parent Node that the token issuing contract issues subnames under - * @returns The Node of the tokenId issued under the parentNode - */ -export const labelHashGeneratedTokenIdToNode = (tokenId: TokenId, parentNode: Node): Node => { - const labelHash: LabelHash = uint256ToHex32(tokenId); - return makeSubdomainNode(labelHash, parentNode); -}; - -/** - * Gets the contracts known to provide tokenized name ownership within the - * specified namespace. - * - * @param namespaceId - The ENSNamespace identifier (e.g. 'mainnet', 'sepolia', 'holesky', 'ens-test-env') - * @returns an array of 0 or more known TokenIssuer for the specified namespace - */ -export const getKnownTokenIssuers = (namespaceId: ENSNamespaceId): TokenIssuer[] => { - const ethBaseRegistrar = maybeGetDatasourceContractChainAddress( - namespaceId, - DatasourceNames.ENSRoot, - "BaseRegistrar", - ); - const nameWrapper = maybeGetDatasourceContractChainAddress( - namespaceId, - DatasourceNames.ENSRoot, - "NameWrapper", - ); - const threeDnsBaseRegistrar = maybeGetDatasourceContractChainAddress( - namespaceId, - DatasourceNames.ThreeDNSBase, - "ThreeDNSToken", - ); - const threeDnsOptimismRegistrar = maybeGetDatasourceContractChainAddress( - namespaceId, - DatasourceNames.ThreeDNSOptimism, - "ThreeDNSToken", - ); - const lineanamesRegistrar = maybeGetDatasourceContractChainAddress( - namespaceId, - DatasourceNames.Lineanames, - "BaseRegistrar", - ); - const basenamesRegistrar = maybeGetDatasourceContractChainAddress( - namespaceId, - DatasourceNames.Basenames, - "BaseRegistrar", - ); - - const result: TokenIssuer[] = []; - - if (ethBaseRegistrar) { - result.push({ - contract: ethBaseRegistrar, - getDomainId: (tokenId: TokenId): Node => { - return labelHashGeneratedTokenIdToNode(tokenId, ETH_NODE); - }, - }); - } - - if (nameWrapper) { - result.push({ - contract: nameWrapper, - getDomainId: (tokenId: TokenId): Node => { - return nameHashGeneratedTokenIdToNode(tokenId); - }, - }); - } - - if (threeDnsBaseRegistrar) { - result.push({ - contract: threeDnsBaseRegistrar, - getDomainId: (tokenId: TokenId): Node => { - return nameHashGeneratedTokenIdToNode(tokenId); - }, - }); - } - - if (threeDnsOptimismRegistrar) { - result.push({ - contract: threeDnsOptimismRegistrar, - getDomainId: (tokenId: TokenId): Node => { - return nameHashGeneratedTokenIdToNode(tokenId); - }, - }); - } - - if (lineanamesRegistrar) { - result.push({ - contract: lineanamesRegistrar, - getDomainId: (tokenId: TokenId): Node => { - return labelHashGeneratedTokenIdToNode(tokenId, LINEANAMES_NODE); - }, - }); - } - - if (basenamesRegistrar) { - result.push({ - contract: basenamesRegistrar, - getDomainId: (tokenId: TokenId): Node => { - return labelHashGeneratedTokenIdToNode(tokenId, BASENAMES_NODE); - }, - }); - } - - return result; -}; - -/** - * Gets the known token issuer for the given ChainAddress in the specified namespace. - * - * @param namespaceId - The ENSNamespace identifier (e.g. 'mainnet', 'sepolia', 'holesky', - * 'ens-test-env') - * @param contract - The ChainAddress to get the known token issuer for - * @returns the known token issuer for the given ChainAddress, or null - * if the ChainAddress is not a known token issuer in the specified namespace - */ -export const getKnownTokenIssuer = ( - namespaceId: ENSNamespaceId, - contract: ChainAddress, -): TokenIssuer | null => { - const tokenIssuers = getKnownTokenIssuers(namespaceId); - return ( - tokenIssuers.find((tokenIssuer) => isChainAddressEqual(tokenIssuer.contract, contract)) ?? null - ); -}; - -/** - * Identifies if the provided ChainAddress is a known token issuer. - * - * @param namespaceId - The ENSNamespace identifier (e.g. 'mainnet', 'sepolia', 'holesky', - * 'ens-test-env') - * @param chainAddress - The ChainAddress to check - * @returns a boolean indicating if the provided ChainAddress is a known token issuer in - * the specified namespace. - */ -export const isKnownTokenIssuer = ( - namespaceId: ENSNamespaceId, - chainAddress: ChainAddress, -): boolean => { - return getKnownTokenIssuer(namespaceId, chainAddress) !== null; -}; - -/** - * Gets the domainId (Node) for a given NFT from contract with tokenId on the specified namespace. - * - * @param namespaceId - The ENSNamespace identifier (e.g. 'mainnet', 'sepolia', 'holesky', - * 'ens-test-env') - * @param contract - The ChainAddress of the NFT contract - * @param tokenId - The tokenId of the NFT - * @returns the domainId (Node) for ENS name associated with the NFT - * @throws an error if the contract is not a known token issuing contract in the specified namespace - */ -export function getDomainIdByTokenId( - namespaceId: ENSNamespaceId, - contract: ChainAddress, - tokenId: TokenId, -): Node { - const tokenIssuers = getKnownTokenIssuers(namespaceId); - const tokenIssuer = tokenIssuers.find((tokenIssuer) => - isChainAddressEqual(tokenIssuer.contract, contract), - ); - if (!tokenIssuer) { - throw new Error( - `The contract at address ${contract.address} on chain ${contract.chainId} is not a known token issuer in the ${namespaceId} namespace`, - ); - } - - return tokenIssuer.getDomainId(tokenId); -} - -/** - * Makes a unique and deterministic event id. - * - * @example `${chainId}-${blockNumber}-${logIndex}` - * - * @param chainId - * @param blockNumber - * @param logIndex - * @returns a unique and deterministic event id. - */ -export const makeEventId = (chainId: ChainId, blockNumber: bigint, logIndex: number) => - [chainId.toString(), blockNumber.toString(), logIndex.toString()].join("-"); - -/** - * Makes a unique and deterministic TokenRef. - * - * @example `${chainId}-${contractAddress}-${tokenId}` - * - * @param chainId - * @param contractAddress - * @param tokenId - * @returns a unique and deterministic TokenRef - */ -export const makeTokenRef = (chainId: ChainId, contractAddress: Address, tokenId: TokenId) => - `${chainId}-${contractAddress}-${uint256ToHex32(tokenId)}`; - -// TODO: Add support for WETH -/** - * Identifiers for supported currencies. - */ -export const CurrencyIds = { - ETH: "ETH", - USDC: "USDC", - DAI: "DAI", -} as const; - -export type CurrencyId = (typeof CurrencyIds)[keyof typeof CurrencyIds]; - -export interface Price { - currency: CurrencyId; - - /** - * The amount of the currency in the smallest unit of the currency. (see - * decimals of the CurrencyConfig for the currency). - * - * Guaranteed to be non-negative. - */ - amount: bigint; -} - -export interface CurrencyConfig { - id: CurrencyId; - name: string; - decimals: number; -} - -const currencyConfigs: Record = { - [CurrencyIds.ETH]: { - id: CurrencyIds.ETH, - name: "Ethereum", - decimals: 18, - }, - [CurrencyIds.USDC]: { - id: CurrencyIds.USDC, - name: "USD Coin", - decimals: 6, - }, - [CurrencyIds.DAI]: { - id: CurrencyIds.DAI, - name: "Dai Stablecoin", - decimals: 18, - }, -} as const; - -export const getCurrencyConfig = (currencyId: CurrencyId): CurrencyConfig => { - return currencyConfigs[currencyId]; -}; - -// NOTE: this mapping currently only considers the subset of chains where we have -// supported token issuing contracts. -const knownCurrencyContracts: Record> = { - /** mainnet namespace */ - [mainnet.id]: { - [CurrencyIds.ETH]: zeroAddress, - [CurrencyIds.USDC]: "0xA0b86a33E6417c5Dd4Baf8C54e5de49E293E9169", - [CurrencyIds.DAI]: "0x6B175474E89094C44Da98b954EedeAC495271d0F", - }, - [base.id]: { - [CurrencyIds.ETH]: zeroAddress, - [CurrencyIds.USDC]: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", - [CurrencyIds.DAI]: "0x50c5725949A6F0c72E6C4a641F24049A917DB0Cb", - }, - [optimism.id]: { - [CurrencyIds.ETH]: zeroAddress, - [CurrencyIds.USDC]: "0x0b2C639c533813f4Aa9D7837CAf62653d097Ff85", - [CurrencyIds.DAI]: "0xDA10009cBd5D07dd0CeCc66161FC93D7c9000da1", - }, - [linea.id]: { - [CurrencyIds.ETH]: zeroAddress, - [CurrencyIds.USDC]: "0x176211869cA2b568f2A7D4EE941E073a821EE1ff", - [CurrencyIds.DAI]: "0x4AF15ec2A0BD43Db75dd04E62FAA3B8EF36b00d5", - }, - /** sepolia namespace */ - [sepolia.id]: { - [CurrencyIds.ETH]: zeroAddress, - [CurrencyIds.USDC]: "0x1c7D4B196Cb0C7B01d743Fbc6116a902379C7238", - [CurrencyIds.DAI]: "0x3e622317f8C93f7328350cF0B56d9eD4C620C5d6", - }, - [baseSepolia.id]: { - [CurrencyIds.ETH]: zeroAddress, - [CurrencyIds.USDC]: "0x036CbD53842c5426634e7929541eC2318f3dCF7e", - [CurrencyIds.DAI]: "0x7368C6C68a4b2b68F90DB2e8F5E3b8E1E5e4F5c7", - }, - [lineaSepolia.id]: { - [CurrencyIds.ETH]: zeroAddress, - [CurrencyIds.USDC]: "0x176211869cA2b568f2A7D4EE941E073a821EE1ff", - [CurrencyIds.DAI]: "0x4AF15ec2A0BD43Db75dd04E62FAA3B8EF36b00d5", - }, - [holesky.id]: { - [CurrencyIds.ETH]: zeroAddress, - [CurrencyIds.USDC]: "0x1c7D4B196Cb0C7B01d743Fbc6116a902379C7238", - [CurrencyIds.DAI]: "0x3e622317f8C93f7328350cF0B56d9eD4C620C5d6", - }, -} as const; - -/** - * Gets the supported currency contracts for a given chain. - * - * @param chainId - The chain ID to get supported currency contracts for - * @returns a record of currency ids to ChainAddresses for the given chain - */ -export const getSupportedCurrencyContractsForChain = ( - chainId: ChainId, -): Record => { - let result = {} as Record; - - const knownCurrencyContractsForChain = knownCurrencyContracts[chainId]; - if (!knownCurrencyContractsForChain) { - return result; - } - - for (const [currencyId, address] of Object.entries(knownCurrencyContractsForChain)) { - result[currencyId as CurrencyId] = { - address, - chainId, - } as ChainAddress; - } - - return result; -}; - -/** - * Gets the supported currency contracts for a given namespace. - * - * @param namespaceId - The ENSNamespace identifier (e.g. 'mainnet', 'sepolia', 'holesky', - * 'ens-test-env') - * @returns a record of currency ids to ChainAddresses for the given namespace - */ -export const getSupportedCurrencyContractsForNamespace = ( - namespaceId: ENSNamespaceId, -): Record => { - let result = {} as Record; - const chainIds = getChainIdsForNamespace(namespaceId); - for (const chainId of chainIds) { - const supportedCurrencyContractsForChain = getSupportedCurrencyContractsForChain(chainId); - result = { ...result, ...supportedCurrencyContractsForChain }; - } - - return result; -}; - -/** - * Identifies if the provided ChainAddress is a supported currency contract in the - * specified namespace. - * - * @param namespaceId - The ENSNamespace identifier (e.g. 'mainnet', 'sepolia', 'holesky', - * 'ens-test-env') - * @param contract - The ChainAddress of the contract to check - * @returns a boolean indicating if the provided ChainAddress is a supported currency - * contract in the specified namespace - */ -export const isSupportedCurrencyContract = ( - namespaceId: ENSNamespaceId, - contract: ChainAddress, -): boolean => { - const supportedCurrencyContracts = getSupportedCurrencyContractsForNamespace(namespaceId); - return Object.values(supportedCurrencyContracts).some((supportedCurrencyContract) => - isChainAddressEqual(supportedCurrencyContract, contract), - ); -}; - -/** - * Gets the currency id for the given contract in the specified namespace. - * - * @param namespaceId - The ENSNamespace identifier (e.g. 'mainnet', 'sepolia', 'holesky', - * 'ens-test-env') - * @param contract - The ChainAddress of the contract to get the currency id for - * @returns the currency id for the given contract in the specified namespace, or - * null if the contract is not a supported currency contract in the - * specified namespace - */ -export const getCurrencyIdForContract = ( - namespaceId: ENSNamespaceId, - contract: ChainAddress, -): CurrencyId | null => { - const supportedCurrencyContracts = getSupportedCurrencyContractsForNamespace(namespaceId); - - for (const [currencyId, supportedCurrencyContract] of Object.entries( - supportedCurrencyContracts, - )) { - if (isChainAddressEqual(supportedCurrencyContract, contract)) { - return currencyId as CurrencyId; - } - } - - return null; -}; - -export interface OnchainEventRef { - eventId: string; - chainId: ChainId; - blockNumber: number; - logIndex: number; - timestamp: UnixTimestamp; - transactionHash: Hex; -} - -export interface SupportedNFT { - tokenType: TokenType; - contract: ChainAddress; - tokenId: TokenId; - domainId: Node; -} - -export interface SupportedPayment { - price: Price; -} - -export interface SupportedSale { - /** - * Event.id set as the unique and deterministic identifier of the onchain event - * associated with the sale. - * - * Composite key format: "{chainId}-{blockNumber}-{logIndex}" (e.g., "1-1234567-5"). - * - * @example "1-1234567-5" - */ - event: OnchainEventRef; - orderHash: Hex; - nft: SupportedNFT; - payment: SupportedPayment; - seller: Address; - buyer: Address; -} - -/** - * Indexes a supported sale transaction - */ -export const indexSupportedSale = async (context: Context, sale: SupportedSale): Promise => { - // Ensure buyer and seller accounts exist - await upsertAccount(context, sale.seller); - await upsertAccount(context, sale.buyer); - - const nameSoldRecord = { - id: sale.event.eventId, - chainId: sale.nft.contract.chainId, - blockNumber: sale.event.blockNumber, - logIndex: sale.event.logIndex, - transactionHash: sale.event.transactionHash, - orderHash: sale.orderHash, - contractAddress: sale.nft.contract.address, - tokenId: uint256ToHex32(sale.nft.tokenId), - tokenType: sale.nft.tokenType, - tokenRef: makeTokenRef(sale.nft.contract.chainId, sale.nft.contract.address, sale.nft.tokenId), - domainId: sale.nft.domainId, - buyer: sale.buyer, - seller: sale.seller, - currency: sale.payment.price.currency, - amount: sale.payment.price.amount, - timestamp: sale.event.timestamp, - } satisfies typeof schema.nameSales.$inferInsert; - - // Index the sale - await context.db.insert(schema.nameSales).values(nameSoldRecord); -}; diff --git a/apps/ensindexer/src/lib/tokenscope/sales.ts b/apps/ensindexer/src/lib/tokenscope/sales.ts new file mode 100644 index 000000000..97ebfd230 --- /dev/null +++ b/apps/ensindexer/src/lib/tokenscope/sales.ts @@ -0,0 +1,42 @@ +import { Price } from "@/lib/currencies"; +import { TokenId, TokenType } from "@/lib/tokenscope/tokens"; +import { ChainAddress, ChainId } from "@ensnode/datasources"; +import { Node, UnixTimestamp } from "@ensnode/ensnode-sdk"; +import { Address, Hex } from "viem"; + +export interface OnchainEventRef { + /** + * Event.id set as the unique and deterministic identifier of the onchain event + * associated with the sale. + * + * Composite key format: "{chainId}-{blockNumber}-{logIndex}" (e.g., "1-1234567-5"). + * + * @example "1-1234567-5" + */ + eventId: string; + chainId: ChainId; + blockNumber: number; + logIndex: number; + timestamp: UnixTimestamp; + transactionHash: Hex; +} + +export interface SupportedNFT { + tokenType: TokenType; + contract: ChainAddress; + tokenId: TokenId; + domainId: Node; +} + +export interface SupportedPayment { + price: Price; +} + +export interface SupportedSale { + event: OnchainEventRef; + orderHash: Hex; + nft: SupportedNFT; + payment: SupportedPayment; + seller: Address; + buyer: Address; +} diff --git a/apps/ensindexer/src/lib/tokenscope/seaport-types.ts b/apps/ensindexer/src/lib/tokenscope/seaport-types.ts new file mode 100644 index 000000000..f3ce04872 --- /dev/null +++ b/apps/ensindexer/src/lib/tokenscope/seaport-types.ts @@ -0,0 +1,136 @@ +import { EventWithArgs } from "@/lib/ponder-helpers"; +import { Address, Hex } from "viem"; + +/** + * Seaport's ItemType enum, re-implemented here to avoid a dependency on seaport-js. + * + * NOTE: if we require further types functionality from seaport-js, we should simply depend on it + * and import them directly. + * + * @see https://github.com/ProjectOpenSea/seaport-js/blob/c4d4756c8000a7143fc1ed9a5aad71b444ae90b4/src/constants.ts#L89 + */ +export enum SeaportItemType { + NATIVE = 0, + ERC20 = 1, + ERC721 = 2, + ERC1155 = 3, + ERC721_WITH_CRITERIA = 4, + ERC1155_WITH_CRITERIA = 5, +} + +/** + * A Seaport OrderFulfilled Event from Ponder, semantically typed with descriptions. + */ +export type SeaportOrderFulfilledEvent = EventWithArgs<{ + /** + * The unique hash identifier of the fulfilled order within Seaport. + * Used to track and reference specific orders on-chain. + */ + orderHash: Hex; + + /** + * The address of the account that created and signed the original order. + * This is the party offering items for trade. + */ + offerer: Address; + + /** + * The address of the zone contract that implements custom validation rules. + * Zones can enforce additional restrictions like allowlists, time windows, + * or other custom logic before order fulfillment. Can be zero address if + * no additional validation is required. + */ + zone: Address; + + /** + * The address that receives the offered items from the order. + * This is typically the order fulfiller or their designated recipient. + */ + recipient: Address; + + /** + * Array of items that the offerer is giving up in this order. + * For listings: NFTs/tokens being sold + * For offers: ETH/ERC20 tokens being offered as payment + */ + offer: readonly SeaportOfferItem[]; + + /** + * Array of items that the offerer expects to receive in return. + * For listings: ETH/ERC20 tokens expected as payment + * For offers: NFTs/tokens being requested in exchange + */ + consideration: readonly SeaportConsiderationItem[]; +}>; + +export type SeaportOfferItem = { + /** + * The type of item in the offer. + * For example, ERC20, ERC721, ERC1155, or NATIVE (ETH) + */ + itemType: SeaportItemType; + + /** + * The contract address of the token. + * - For ERC721/ERC1155: The NFT contract address + * - For ERC20: The token contract address + * - For NATIVE (ETH): Zero address (0x0000000000000000000000000000000000000000) + */ + token: Address; + + /** + * The identifier field has different meanings based on itemType: + * - For ERC721/ERC1155: The specific token ID of the NFT + * - For ERC20: Always 0 (not used for fungible tokens) + * - For NATIVE (ETH): Always 0 (not used for native currency) + */ + identifier: bigint; + + /** + * The amount field has different meanings based on itemType: + * - For ERC721: Always 1 (you can only transfer 1 unique NFT) + * - For ERC1155: The quantity of tokens with the specified identifier (for our purposes, always 1) + * - For ERC20: The amount of tokens (in wei/smallest unit) + * - For NATIVE (ETH): The amount of ETH (in wei) + */ + amount: bigint; +}; + +export type SeaportConsiderationItem = { + /** + * The type of item in the consideration. + * For example, ERC20, ERC721, ERC1155, or NATIVE (ETH) + */ + itemType: SeaportItemType; + + /** + * The contract address of the token. + * - For ERC721/ERC1155: The NFT contract address + * - For ERC20: The token contract address + * - For NATIVE (ETH): Zero address (0x0000000000000000000000000000000000000000) + */ + token: Address; + + /** + * The identifier field has different meanings based on itemType: + * - For ERC721/ERC1155: The specific token ID of the NFT + * - For ERC20: Always 0 (not used for fungible tokens) + * - For NATIVE (ETH): Always 0 (not used for native currency) + */ + identifier: bigint; + + /** + * The amount field has different meanings based on itemType: + * - For ERC721: Always 1 (you can only transfer 1 unique NFT) + * - For ERC1155: The quantity of tokens with the specified identifier + * - For ERC20: The amount of tokens (in wei/smallest unit) + * - For NATIVE (ETH): The amount of ETH (in wei) + */ + amount: bigint; + + /** + * The address that receives the consideration items from the order. + * This is typically the order fulfiller or their designated recipient. + */ + recipient: Address; +}; diff --git a/apps/ensindexer/src/handlers/Seaport.ts b/apps/ensindexer/src/lib/tokenscope/seaport.ts similarity index 57% rename from apps/ensindexer/src/handlers/Seaport.ts rename to apps/ensindexer/src/lib/tokenscope/seaport.ts index 6afffec9d..b08338a03 100644 --- a/apps/ensindexer/src/handlers/Seaport.ts +++ b/apps/ensindexer/src/lib/tokenscope/seaport.ts @@ -1,153 +1,29 @@ -import { Context } from "ponder:registry"; -import { ItemType as SeaportItemType } from "@opensea/seaport-js/lib/constants"; - -import config from "@/config"; -import { EventWithArgs } from "@/lib/ponder-helpers"; +import { CurrencyIds, getCurrencyIdForContract } from "@/lib/currencies"; +import { makeEventId } from "@/lib/ids"; import { - CurrencyIds, OnchainEventRef, SupportedNFT, SupportedPayment, SupportedSale, - TokenType, - TokenTypes, - getCurrencyIdForContract, - getKnownTokenIssuer, - indexSupportedSale, - makeEventId, -} from "@/lib/tokenscope-helpers"; +} from "@/lib/tokenscope/sales"; +import { + SeaportConsiderationItem, + SeaportItemType, + SeaportOfferItem, + SeaportOrderFulfilledEvent, +} from "@/lib/tokenscope/seaport-types"; +import { getKnownTokenIssuer } from "@/lib/tokenscope/token-issuers"; +import { TokenType, TokenTypes } from "@/lib/tokenscope/tokens"; import { ChainId, ENSNamespaceId } from "@ensnode/datasources"; -import { Address, Hex } from "viem"; /** - * The file has the responsibility for logic that maps from Seaport-specific data models - * into our more generic TokenScope data models as found in the `@/lib/tokenscope-helpers` file. - * - * Seaport's data model supports complexity that has more negatives than benefits. TokenScope aims - * to deliver a more simple data model for developers to build ENS apps with. This simplified data - * model is anticipated to still support the vast majority of real-world use cases. - * - * In this file we examine each indexed Seaport event to determine if it fits within the TokenScope - * data model. If it does, we extract the relevant data and map it into the TokenScope data model. + * The file has the responsibility of maping from Seaport-specific data models into our more generic + * TokenScope data models as found in the `@/lib/tokenscope-helpers` file. * - * If it does not, we ignore the event. + * TokenScope aims to deliver a simpler datamodel than Seaport provides but still support the + * majority of real-world use cases. */ -type SeaportOfferItem = { - /** - * The type of item in the offer. - * For example, ERC20, ERC721, ERC1155, or NATIVE (ETH) - */ - itemType: SeaportItemType; - - /** - * The contract address of the token. - * - For ERC721/ERC1155: The NFT contract address - * - For ERC20: The token contract address - * - For NATIVE (ETH): Zero address (0x0000000000000000000000000000000000000000) - */ - token: Address; - - /** - * The identifier field has different meanings based on itemType: - * - For ERC721/ERC1155: The specific token ID of the NFT - * - For ERC20: Always 0 (not used for fungible tokens) - * - For NATIVE (ETH): Always 0 (not used for native currency) - */ - identifier: bigint; - - /** - * The amount field has different meanings based on itemType: - * - For ERC721: Always 1 (you can only transfer 1 unique NFT) - * - For ERC1155: The quantity of tokens with the specified identifier (for our purposes, always 1) - * - For ERC20: The amount of tokens (in wei/smallest unit) - * - For NATIVE (ETH): The amount of ETH (in wei) - */ - amount: bigint; -}; - -type SeaportConsiderationItem = { - /** - * The type of item in the consideration. - * For example, ERC20, ERC721, ERC1155, or NATIVE (ETH) - */ - itemType: SeaportItemType; - - /** - * The contract address of the token. - * - For ERC721/ERC1155: The NFT contract address - * - For ERC20: The token contract address - * - For NATIVE (ETH): Zero address (0x0000000000000000000000000000000000000000) - */ - token: Address; - - /** - * The identifier field has different meanings based on itemType: - * - For ERC721/ERC1155: The specific token ID of the NFT - * - For ERC20: Always 0 (not used for fungible tokens) - * - For NATIVE (ETH): Always 0 (not used for native currency) - */ - identifier: bigint; - - /** - * The amount field has different meanings based on itemType: - * - For ERC721: Always 1 (you can only transfer 1 unique NFT) - * - For ERC1155: The quantity of tokens with the specified identifier - * - For ERC20: The amount of tokens (in wei/smallest unit) - * - For NATIVE (ETH): The amount of ETH (in wei) - */ - amount: bigint; - - /** - * The address that receives the consideration items from the order. - * This is typically the order fulfiller or their designated recipient. - */ - recipient: Address; -}; - -interface SeaportOrderFulfilledEvent - extends EventWithArgs<{ - /** - * The unique hash identifier of the fulfilled order within Seaport. - * Used to track and reference specific orders on-chain. - */ - orderHash: Hex; - - /** - * The address of the account that created and signed the original order. - * This is the party offering items for trade. - */ - offerer: Address; - - /** - * The address of the zone contract that implements custom validation rules. - * Zones can enforce additional restrictions like allowlists, time windows, - * or other custom logic before order fulfillment. Can be zero address if - * no additional validation is required. - */ - zone: Address; - - /** - * The address that receives the offered items from the order. - * This is typically the order fulfiller or their designated recipient. - */ - recipient: Address; - - /** - * Array of items that the offerer is giving up in this order. - * For listings: NFTs/tokens being sold - * For offers: ETH/ERC20 tokens being offered as payment - */ - offer: readonly SeaportOfferItem[]; - - /** - * Array of items that the offerer expects to receive in return. - * For listings: ETH/ERC20 tokens expected as payment - * For offers: NFTs/tokens being requested in exchange - */ - consideration: readonly SeaportConsiderationItem[]; - }> {} - /** * Gets the supported TokenScope token type for a given Seaport item type. * @@ -344,7 +220,7 @@ const buildOnchainEventRef = ( } satisfies OnchainEventRef; }; -const getSupportedSale = ( +export const getSupportedSaleFromOrderFulfilledEvent = ( namespaceId: ENSNamespaceId, chainId: ChainId, event: SeaportOrderFulfilledEvent, @@ -367,14 +243,14 @@ const getSupportedSale = ( const consolidatedOfferPayment = consolidateSupportedPayments(offerPayments); const consolidatedConsiderationPayment = consolidateSupportedPayments(considerationPayments); + // offer is exactly 1 supported NFT and consideration consolidates to 1 supported payment + // therefore the offerer is the seller and the recipient is the buyer if ( consolidatedOfferNFT && !consolidatedConsiderationNFT && consolidatedOfferPayment && !consolidatedConsiderationPayment ) { - // offer is exactly 1 supported NFT and consideration consolidates to 1 supported payment - // therefore the offerer is the seller and the recipient is the buyer return { event: buildOnchainEventRef(chainId, event), orderHash, @@ -383,14 +259,16 @@ const getSupportedSale = ( seller: offerer, buyer: recipient, } satisfies SupportedSale; - } else if ( + } + + // consideration is exactly 1 supported NFT and offer consolidates to 1 supported payment + // therefore the recipient is the seller and the offerer is the buyer + if ( !consolidatedOfferNFT && consolidatedConsiderationNFT && !consolidatedOfferPayment && consolidatedConsiderationPayment ) { - // consideration is exactly 1 supported NFT and offer consolidates to 1 supported payment - // therefore the recipient is the seller and the offerer is the buyer return { event: buildOnchainEventRef(chainId, event), orderHash, @@ -399,24 +277,8 @@ const getSupportedSale = ( seller: recipient, buyer: offerer, } satisfies SupportedSale; - } else { - // unsupported sale - return null; } -}; -/** - * Handles each Seaport OrderFulfilled event - */ -export async function handleOrderFulfilled({ - context, - event, -}: { - context: Context; - event: SeaportOrderFulfilledEvent; -}) { - const supportedSale = getSupportedSale(config.namespace, context.chain.id, event); - if (supportedSale) { - await indexSupportedSale(context, supportedSale); - } -} + // otherwise, unsupported sale + return null; +}; diff --git a/apps/ensindexer/src/lib/tokenscope/token-issuers.ts b/apps/ensindexer/src/lib/tokenscope/token-issuers.ts new file mode 100644 index 000000000..389c44c73 --- /dev/null +++ b/apps/ensindexer/src/lib/tokenscope/token-issuers.ts @@ -0,0 +1,221 @@ +import { TokenId } from "@/lib/tokenscope/tokens"; +import { + ChainAddress, + DatasourceNames, + ENSNamespaceId, + isChainAddressEqual, + maybeGetDatasourceContractChainAddress, +} from "@ensnode/datasources"; +import { + BASENAMES_NODE, + ETH_NODE, + LINEANAMES_NODE, + LabelHash, + type Node, + makeSubdomainNode, + uint256ToHex32, +} from "@ensnode/ensnode-sdk"; + +/** + * A contract that issues tokenized ENS names. + */ +export interface TokenIssuer { + /** + * The ChainAddress of the token issuer contract. + */ + contract: ChainAddress; + + /** + * Applies the token issuer contract's logic for converting from the token id + * representation of a domain to the domain id (Node) representation of a domain. + */ + getDomainId: (tokenId: TokenId) => Node; +} + +/** + * Converts the tokenId from an ENS name token-issuing contract to a Node + * for the case that the contract generates each tokenId using namehash of + * the full name. + * + * @param tokenId - The tokenId to convert + * @returns The Node of the tokenId + */ +export const nameHashGeneratedTokenIdToNode = (tokenId: TokenId): Node => { + return uint256ToHex32(tokenId); +}; + +/** + * Converts the tokenId from an ENS name token-issuing contract to a Node + * for the case that the contract generates each tokenId using labelhash of + * the direct subname of the parent node. + * + * @param tokenId - The tokenId to convert + * @param parentNode - the parent Node that the token issuing contract issues subnames under + * @returns The Node of the tokenId issued under the parentNode + */ +export const labelHashGeneratedTokenIdToNode = (tokenId: TokenId, parentNode: Node): Node => { + const labelHash: LabelHash = uint256ToHex32(tokenId); + return makeSubdomainNode(labelHash, parentNode); +}; + +/** + * Gets the contracts known to provide tokenized name ownership within the + * specified namespace. + * + * @param namespaceId - The ENSNamespace identifier (e.g. 'mainnet', 'sepolia', 'holesky', 'ens-test-env') + * @returns an array of 0 or more known TokenIssuer for the specified namespace + */ +export const getKnownTokenIssuers = (namespaceId: ENSNamespaceId): TokenIssuer[] => { + const ethBaseRegistrar = maybeGetDatasourceContractChainAddress( + namespaceId, + DatasourceNames.ENSRoot, + "BaseRegistrar", + ); + const nameWrapper = maybeGetDatasourceContractChainAddress( + namespaceId, + DatasourceNames.ENSRoot, + "NameWrapper", + ); + const threeDnsBaseRegistrar = maybeGetDatasourceContractChainAddress( + namespaceId, + DatasourceNames.ThreeDNSBase, + "ThreeDNSToken", + ); + const threeDnsOptimismRegistrar = maybeGetDatasourceContractChainAddress( + namespaceId, + DatasourceNames.ThreeDNSOptimism, + "ThreeDNSToken", + ); + const lineanamesRegistrar = maybeGetDatasourceContractChainAddress( + namespaceId, + DatasourceNames.Lineanames, + "BaseRegistrar", + ); + const basenamesRegistrar = maybeGetDatasourceContractChainAddress( + namespaceId, + DatasourceNames.Basenames, + "BaseRegistrar", + ); + + const result: TokenIssuer[] = []; + + if (ethBaseRegistrar) { + result.push({ + contract: ethBaseRegistrar, + getDomainId: (tokenId: TokenId): Node => { + return labelHashGeneratedTokenIdToNode(tokenId, ETH_NODE); + }, + }); + } + + if (nameWrapper) { + result.push({ + contract: nameWrapper, + getDomainId: (tokenId: TokenId): Node => { + return nameHashGeneratedTokenIdToNode(tokenId); + }, + }); + } + + if (threeDnsBaseRegistrar) { + result.push({ + contract: threeDnsBaseRegistrar, + getDomainId: (tokenId: TokenId): Node => { + return nameHashGeneratedTokenIdToNode(tokenId); + }, + }); + } + + if (threeDnsOptimismRegistrar) { + result.push({ + contract: threeDnsOptimismRegistrar, + getDomainId: (tokenId: TokenId): Node => { + return nameHashGeneratedTokenIdToNode(tokenId); + }, + }); + } + + if (lineanamesRegistrar) { + result.push({ + contract: lineanamesRegistrar, + getDomainId: (tokenId: TokenId): Node => { + return labelHashGeneratedTokenIdToNode(tokenId, LINEANAMES_NODE); + }, + }); + } + + if (basenamesRegistrar) { + result.push({ + contract: basenamesRegistrar, + getDomainId: (tokenId: TokenId): Node => { + return labelHashGeneratedTokenIdToNode(tokenId, BASENAMES_NODE); + }, + }); + } + + return result; +}; + +/** + * Gets the known token issuer for the given ChainAddress in the specified namespace. + * + * @param namespaceId - The ENSNamespace identifier (e.g. 'mainnet', 'sepolia', 'holesky', + * 'ens-test-env') + * @param contract - The ChainAddress to get the known token issuer for + * @returns the known token issuer for the given ChainAddress, or null + * if the ChainAddress is not a known token issuer in the specified namespace + */ +export const getKnownTokenIssuer = ( + namespaceId: ENSNamespaceId, + contract: ChainAddress, +): TokenIssuer | null => { + const tokenIssuers = getKnownTokenIssuers(namespaceId); + return ( + tokenIssuers.find((tokenIssuer) => isChainAddressEqual(tokenIssuer.contract, contract)) ?? null + ); +}; + +/** + * Identifies if the provided ChainAddress is a known token issuer. + * + * @param namespaceId - The ENSNamespace identifier (e.g. 'mainnet', 'sepolia', 'holesky', + * 'ens-test-env') + * @param chainAddress - The ChainAddress to check + * @returns a boolean indicating if the provided ChainAddress is a known token issuer in + * the specified namespace. + */ +export const isKnownTokenIssuer = ( + namespaceId: ENSNamespaceId, + chainAddress: ChainAddress, +): boolean => { + return getKnownTokenIssuer(namespaceId, chainAddress) !== null; +}; + +/** + * Gets the domainId (Node) for a given NFT from contract with tokenId on the specified namespace. + * + * @param namespaceId - The ENSNamespace identifier (e.g. 'mainnet', 'sepolia', 'holesky', + * 'ens-test-env') + * @param contract - The ChainAddress of the NFT contract + * @param tokenId - The tokenId of the NFT + * @returns the domainId (Node) for ENS name associated with the NFT + * @throws an error if the contract is not a known token issuing contract in the specified namespace + */ +export function getDomainIdByTokenId( + namespaceId: ENSNamespaceId, + contract: ChainAddress, + tokenId: TokenId, +): Node { + const tokenIssuers = getKnownTokenIssuers(namespaceId); + const tokenIssuer = tokenIssuers.find((tokenIssuer) => + isChainAddressEqual(tokenIssuer.contract, contract), + ); + + if (!tokenIssuer) { + throw new Error( + `The contract at address ${contract.address} on chain ${contract.chainId} is not a known token issuer in the ${namespaceId} namespace`, + ); + } + + return tokenIssuer.getDomainId(tokenId); +} diff --git a/apps/ensindexer/src/lib/tokenscope/tokens.ts b/apps/ensindexer/src/lib/tokenscope/tokens.ts new file mode 100644 index 000000000..e8146b503 --- /dev/null +++ b/apps/ensindexer/src/lib/tokenscope/tokens.ts @@ -0,0 +1,28 @@ +import { ChainId } from "@ensnode/datasources"; +import { uint256ToHex32 } from "@ensnode/ensnode-sdk"; +import { Address } from "viem"; + +export const TokenTypes = { + ERC721: "ERC721", + ERC1155: "ERC1155", +} as const; + +export type TokenType = (typeof TokenTypes)[keyof typeof TokenTypes]; + +/** + * A uint256 value that identifies a specific token within a NFT contract. + */ +export type TokenId = bigint; + +/** + * Makes a unique and deterministic TokenRef. + * + * @example `${chainId}-${contractAddress}-${tokenId}` + * + * @param chainId + * @param contractAddress + * @param tokenId + * @returns a unique and deterministic TokenRef + */ +export const makeTokenRef = (chainId: ChainId, contractAddress: Address, tokenId: TokenId) => + `${chainId}-${contractAddress}-${uint256ToHex32(tokenId)}`; diff --git a/apps/ensindexer/src/plugins/tokenscope/handlers/Seaport.ts b/apps/ensindexer/src/plugins/tokenscope/handlers/Seaport.ts index 88b3db297..df898a8bc 100644 --- a/apps/ensindexer/src/plugins/tokenscope/handlers/Seaport.ts +++ b/apps/ensindexer/src/plugins/tokenscope/handlers/Seaport.ts @@ -1,9 +1,12 @@ import { ponder } from "ponder:registry"; - +import schema from "ponder:schema"; import { PluginName } from "@ensnode/ensnode-sdk"; -import { handleOrderFulfilled } from "@/handlers/Seaport"; +import config from "@/config"; +import { upsertAccount } from "@/lib/db-helpers"; import { namespaceContract } from "@/lib/plugin-helpers"; +import { getSupportedSaleFromOrderFulfilledEvent } from "@/lib/tokenscope/seaport"; +import { makeTokenRef } from "@/lib/tokenscope/tokens"; /** * Registers event handlers with Ponder. @@ -11,5 +14,38 @@ import { namespaceContract } from "@/lib/plugin-helpers"; export default function () { const pluginName = PluginName.TokenScope; - ponder.on(namespaceContract(pluginName, "Seaport:OrderFulfilled"), handleOrderFulfilled); + ponder.on(namespaceContract(pluginName, "Seaport:OrderFulfilled"), async ({ context, event }) => { + const sale = getSupportedSaleFromOrderFulfilledEvent(config.namespace, context.chain.id, event); + + // no supported sale detected in event, no-op + if (!sale) return; + + // upsert buyer and seller accounts + await upsertAccount(context, sale.seller); + await upsertAccount(context, sale.buyer); + + // insert NameSale entity + await context.db.insert(schema.nameSales).values({ + id: sale.event.eventId, + chainId: sale.nft.contract.chainId, + blockNumber: sale.event.blockNumber, + logIndex: sale.event.logIndex, + transactionHash: sale.event.transactionHash, + orderHash: sale.orderHash, + contractAddress: sale.nft.contract.address, + tokenId: sale.nft.tokenId, + tokenType: sale.nft.tokenType, + tokenRef: makeTokenRef( + sale.nft.contract.chainId, + sale.nft.contract.address, + sale.nft.tokenId, + ), + domainId: sale.nft.domainId, + buyer: sale.buyer, + seller: sale.seller, + currency: sale.payment.price.currency, + amount: sale.payment.price.amount, + timestamp: sale.event.timestamp, + }); + }); } diff --git a/packages/ensnode-schema/src/tokenscope.schema.ts b/packages/ensnode-schema/src/tokenscope.schema.ts index c0f598e41..2ab5ab1e8 100644 --- a/packages/ensnode-schema/src/tokenscope.schema.ts +++ b/packages/ensnode-schema/src/tokenscope.schema.ts @@ -1,4 +1,3 @@ -import schema from "ponder:schema"; import { index, onchainTable } from "ponder"; export const nameSales = onchainTable( @@ -50,10 +49,8 @@ export const nameSales = onchainTable( * Interpretation depends on 'tokenType': * - ERC721: Unique token within contract * - ERC1155: Token type identifier (multiple copies may exist) - * - * The tokenId is formatted as a hex string representing a uint256. */ - tokenId: t.text().notNull(), + tokenId: t.bigint().notNull(), /** * The type of token that was sold (ERC721 or ERC1155). From 6ef1dead9d854db590750b623635c8c95f7f3705 Mon Sep 17 00:00:00 2001 From: shrugs Date: Mon, 25 Aug 2025 16:08:48 -0500 Subject: [PATCH 36/61] fix: streamline some conditionals --- apps/ensindexer/src/lib/tokenscope/seaport.ts | 48 ++++++++----------- 1 file changed, 20 insertions(+), 28 deletions(-) diff --git a/apps/ensindexer/src/lib/tokenscope/seaport.ts b/apps/ensindexer/src/lib/tokenscope/seaport.ts index b08338a03..df9d54d46 100644 --- a/apps/ensindexer/src/lib/tokenscope/seaport.ts +++ b/apps/ensindexer/src/lib/tokenscope/seaport.ts @@ -15,14 +15,7 @@ import { import { getKnownTokenIssuer } from "@/lib/tokenscope/token-issuers"; import { TokenType, TokenTypes } from "@/lib/tokenscope/tokens"; import { ChainId, ENSNamespaceId } from "@ensnode/datasources"; - -/** - * The file has the responsibility of maping from Seaport-specific data models into our more generic - * TokenScope data models as found in the `@/lib/tokenscope-helpers` file. - * - * TokenScope aims to deliver a simpler datamodel than Seaport provides but still support the - * majority of real-world use cases. - */ +import { uniq } from "@ensnode/ensnode-sdk"; /** * Gets the supported TokenScope token type for a given Seaport item type. @@ -58,18 +51,14 @@ const getSupportedNFT = ( ): SupportedNFT | null => { // validate item as an ERC721/ERC1155 NFT const tokenType = getSupportedTokenType(item.itemType); - if (!tokenType) { - return null; - } + if (!tokenType) return null; // validate that the token is a known token issuing contract const tokenIssuer = getKnownTokenIssuer(namespaceId, { chainId, address: item.token, }); - if (!tokenIssuer) { - return null; - } + if (!tokenIssuer) return null; const contract = tokenIssuer.contract; const tokenId = item.identifier; @@ -95,9 +84,8 @@ const getSupportedPayment = ( // validate that the item is a supported currency const currencyId = getCurrencyIdForContract(namespaceId, currencyContract); - if (!currencyId) { - return null; // Unsupported currency - } + + if (!currencyId) return null; // Unsupported currency // validate the Seaport item type is supported and matches the currencyId if (item.itemType === SeaportItemType.NATIVE) { @@ -113,9 +101,7 @@ const getSupportedPayment = ( return null; } - if (item.amount < 0n) { - return null; // Invalid amount - } + if (item.amount < 0n) return null; // Invalid amount return { price: { @@ -167,20 +153,18 @@ const getSeaportItemExtractions = ( }; const consolidateSupportedNFTs = (nfts: SupportedNFT[]): SupportedNFT | null => { - if (nfts.length !== 1) { - return null; // Either no NFT or multiple NFTs - } - + // Either no NFT or multiple NFTs + if (nfts.length !== 1) return null; return nfts[0]!; }; const consolidateSupportedPayments = (payments: SupportedPayment[]): SupportedPayment | null => { // Get the set of distinct currencies in the payment const paymentCurrencies = payments.map((payment) => payment.price.currency); - const uniqueCurrencies = [...new Set(paymentCurrencies)]; - if (uniqueCurrencies.length !== 1) { - return null; // Either no payment or multiple payments in mixed currencies - } + const uniqueCurrencies = uniq(paymentCurrencies); + + // Either no payment or multiple payments in mixed currencies + if (uniqueCurrencies.length !== 1) return null; // consolidate multiple payments in the same currency into one. const totalAmount = payments.reduce((total, payment) => total + payment.price.amount, 0n); @@ -202,11 +186,13 @@ const buildOnchainEventRef = ( `Error building onchain event ref: block timestamp is too large: ${event.block.timestamp}`, ); } + if (event.block.number > BigInt(Number.MAX_SAFE_INTEGER)) { throw new Error( `Error building onchain event ref: block number is too large: ${event.block.number}`, ); } + const blockNumber = Number(event.block.number); const timestamp = Number(event.block.timestamp); @@ -220,6 +206,11 @@ const buildOnchainEventRef = ( } satisfies OnchainEventRef; }; +/** + * Maps from Seaport-specific OrderFulfilled event into our more generic TokenScope `SupportedSale`, + * if possible. TokenScope aims to deliver a simpler datamodel than Seaport provides but still + * support the majority of real-world use cases. + */ export const getSupportedSaleFromOrderFulfilledEvent = ( namespaceId: ENSNamespaceId, chainId: ChainId, @@ -232,6 +223,7 @@ export const getSupportedSaleFromOrderFulfilledEvent = ( chainId, offer, ); + const { nfts: considerationNFTs, payments: considerationPayments } = getSeaportItemExtractions( namespaceId, chainId, From 469d36d7318aa108c9d75b8187fd0fe7841c15ae Mon Sep 17 00:00:00 2001 From: shrugs Date: Mon, 25 Aug 2025 16:59:18 -0500 Subject: [PATCH 37/61] refactor: move stuff out of datasources, fix handler placement, refactor contract ids to be CAIP-standard --- apps/ensindexer/package.json | 1 + apps/ensindexer/src/lib/currencies.ts | 48 +++--- apps/ensindexer/src/lib/datasource-helpers.ts | 53 ++++++ apps/ensindexer/src/lib/ponder-helpers.ts | 1 - apps/ensindexer/src/lib/tokenscope/assets.ts | 14 ++ apps/ensindexer/src/lib/tokenscope/sales.ts | 28 +-- apps/ensindexer/src/lib/tokenscope/seaport.ts | 78 +++------ .../src/lib/tokenscope/token-issuers.ts | 55 ++---- apps/ensindexer/src/lib/tokenscope/tokens.ts | 28 --- .../plugins/tokenscope/handlers/Seaport.ts | 48 ++++-- packages/datasources/src/lib/types.ts | 27 --- packages/datasources/src/namespaces.ts | 45 ----- .../ensnode-schema/src/tokenscope.schema.ts | 15 +- .../src/ensindexer/config/types.ts | 11 +- packages/ensnode-sdk/src/shared/account-id.ts | 9 + packages/ensnode-sdk/src/shared/index.ts | 1 + packages/ensnode-sdk/src/shared/types.ts | 21 ++- pnpm-lock.yaml | 161 +++++++----------- 18 files changed, 265 insertions(+), 379 deletions(-) create mode 100644 apps/ensindexer/src/lib/datasource-helpers.ts create mode 100644 apps/ensindexer/src/lib/tokenscope/assets.ts delete mode 100644 apps/ensindexer/src/lib/tokenscope/tokens.ts create mode 100644 packages/ensnode-sdk/src/shared/account-id.ts diff --git a/apps/ensindexer/package.json b/apps/ensindexer/package.json index 88f229b3b..25bb025f7 100644 --- a/apps/ensindexer/package.json +++ b/apps/ensindexer/package.json @@ -43,6 +43,7 @@ "@opentelemetry/sdk-trace-node": "^2.0.1", "@opentelemetry/semantic-conventions": "^1.34.0", "@types/dns-packet": "^5.6.5", + "caip": "^1.1.1", "date-fns": "catalog:", "deepmerge-ts": "^7.1.5", "dns-packet": "^5.6.1", diff --git a/apps/ensindexer/src/lib/currencies.ts b/apps/ensindexer/src/lib/currencies.ts index b2c049cae..d0eed5c52 100644 --- a/apps/ensindexer/src/lib/currencies.ts +++ b/apps/ensindexer/src/lib/currencies.ts @@ -9,13 +9,9 @@ import { sepolia, } from "viem/chains"; -import { - ChainAddress, - ChainId, - ENSNamespaceId, - getChainIdsForNamespace, - isChainAddressEqual, -} from "@ensnode/datasources"; +import { getChainIdsInNamespace } from "@/lib/datasource-helpers"; +import { ENSNamespaceId, getENSNamespace } from "@ensnode/datasources"; +import { AccountId, ChainId, accountIdEqual, uniq } from "@ensnode/ensnode-sdk"; import { Address, zeroAddress } from "viem"; /** @@ -95,6 +91,7 @@ const knownCurrencyContracts: Record> = { [CurrencyIds.USDC]: "0x176211869cA2b568f2A7D4EE941E073a821EE1ff", [CurrencyIds.DAI]: "0x4AF15ec2A0BD43Db75dd04E62FAA3B8EF36b00d5", }, + /** sepolia namespace */ [sepolia.id]: { [CurrencyIds.ETH]: zeroAddress, @@ -122,23 +119,21 @@ const knownCurrencyContracts: Record> = { * Gets the supported currency contracts for a given chain. * * @param chainId - The chain ID to get supported currency contracts for - * @returns a record of currency ids to ChainAddresses for the given chain + * @returns a record of currency ids to AccountIds for the given chain */ export const getSupportedCurrencyContractsForChain = ( chainId: ChainId, -): Record => { - let result = {} as Record; +): Record => { + let result = {} as Record; const knownCurrencyContractsForChain = knownCurrencyContracts[chainId]; - if (!knownCurrencyContractsForChain) { - return result; - } + if (!knownCurrencyContractsForChain) return result; for (const [currencyId, address] of Object.entries(knownCurrencyContractsForChain)) { result[currencyId as CurrencyId] = { address, chainId, - } as ChainAddress; + } as AccountId; } return result; @@ -149,13 +144,13 @@ export const getSupportedCurrencyContractsForChain = ( * * @param namespaceId - The ENSNamespace identifier (e.g. 'mainnet', 'sepolia', 'holesky', * 'ens-test-env') - * @returns a record of currency ids to ChainAddresses for the given namespace + * @returns a record of currency ids to AccountIds for the given namespace */ export const getSupportedCurrencyContractsForNamespace = ( namespaceId: ENSNamespaceId, -): Record => { - let result = {} as Record; - const chainIds = getChainIdsForNamespace(namespaceId); +): Record => { + let result = {} as Record; + const chainIds = getChainIdsInNamespace(namespaceId); for (const chainId of chainIds) { const supportedCurrencyContractsForChain = getSupportedCurrencyContractsForChain(chainId); result = { ...result, ...supportedCurrencyContractsForChain }; @@ -165,22 +160,21 @@ export const getSupportedCurrencyContractsForNamespace = ( }; /** - * Identifies if the provided ChainAddress is a supported currency contract in the - * specified namespace. + * Identifies if the provided AccountId is a supported currency contract in the specified namespace. * * @param namespaceId - The ENSNamespace identifier (e.g. 'mainnet', 'sepolia', 'holesky', * 'ens-test-env') - * @param contract - The ChainAddress of the contract to check - * @returns a boolean indicating if the provided ChainAddress is a supported currency + * @param contract - The AccountId of the contract to check + * @returns a boolean indicating if the provided AccountId is a supported currency * contract in the specified namespace */ export const isSupportedCurrencyContract = ( namespaceId: ENSNamespaceId, - contract: ChainAddress, + contract: AccountId, ): boolean => { const supportedCurrencyContracts = getSupportedCurrencyContractsForNamespace(namespaceId); return Object.values(supportedCurrencyContracts).some((supportedCurrencyContract) => - isChainAddressEqual(supportedCurrencyContract, contract), + accountIdEqual(supportedCurrencyContract, contract), ); }; @@ -189,21 +183,21 @@ export const isSupportedCurrencyContract = ( * * @param namespaceId - The ENSNamespace identifier (e.g. 'mainnet', 'sepolia', 'holesky', * 'ens-test-env') - * @param contract - The ChainAddress of the contract to get the currency id for + * @param contract - The AccountId of the contract to get the currency id for * @returns the currency id for the given contract in the specified namespace, or * null if the contract is not a supported currency contract in the * specified namespace */ export const getCurrencyIdForContract = ( namespaceId: ENSNamespaceId, - contract: ChainAddress, + contract: AccountId, ): CurrencyId | null => { const supportedCurrencyContracts = getSupportedCurrencyContractsForNamespace(namespaceId); for (const [currencyId, supportedCurrencyContract] of Object.entries( supportedCurrencyContracts, )) { - if (isChainAddressEqual(supportedCurrencyContract, contract)) { + if (accountIdEqual(supportedCurrencyContract, contract)) { return currencyId as CurrencyId; } } diff --git a/apps/ensindexer/src/lib/datasource-helpers.ts b/apps/ensindexer/src/lib/datasource-helpers.ts new file mode 100644 index 000000000..9fab29936 --- /dev/null +++ b/apps/ensindexer/src/lib/datasource-helpers.ts @@ -0,0 +1,53 @@ +import { + DatasourceName, + ENSNamespaceId, + getENSNamespace, + maybeGetDatasource, +} from "@ensnode/datasources"; +import { AccountId, ChainId, uniq } from "@ensnode/ensnode-sdk"; + +/** + * Gets all the distinct chainIds used in the specified namespace. + * + * NOTE: This takes no consideration of which datasources are configured for indexing. + * + * @param namespaceId - The ENSNamespace identifier (e.g. 'mainnet', 'sepolia', 'holesky', + * 'ens-test-env') + * @returns an array of distinct chainIds with datasources in the specified namespace + */ +export const getChainIdsInNamespace = (namespaceId: ENSNamespaceId): ChainId[] => { + const namespace = getENSNamespace(namespaceId); + const chainIds = Object.values(namespace).map((datasource) => datasource.chain.id); + return uniq(chainIds); +}; + +/** + * Gets the chain address for the specified namespace, datasource, and + * contract name, or undefined if it is not defined or is not a single chain address. + * + * This is useful when you want to retrieve a single ChainAddress for an arbitrary contract + * where it may or may not actually be defined. + * + * @param namespaceId - The ENSNamespace identifier (e.g. 'mainnet', 'sepolia', 'holesky', + * 'ens-test-env') + * @param datasourceName - The name of the Datasource to search for contractName in + * @param contractName - The name of the contract to retrieve the chain address for + * @returns The ChainAddress for the given namespace, datasource, and contract + * name, or undefined if it does not exist or is not a single chain address + */ +export const maybeGetDatasourceContract = ( + namespaceId: ENSNamespaceId, + datasourceName: DatasourceName, + contractName: string, +): AccountId | undefined => { + const datasource = maybeGetDatasource(namespaceId, datasourceName); + if (!datasource) return undefined; + + const maybeAddress = datasource.contracts[contractName]?.address; + if (maybeAddress === undefined || Array.isArray(maybeAddress)) return undefined; + + return { + chainId: datasource.chain.id, + address: maybeAddress, + }; +}; diff --git a/apps/ensindexer/src/lib/ponder-helpers.ts b/apps/ensindexer/src/lib/ponder-helpers.ts index 4439c1e2c..25a3403d8 100644 --- a/apps/ensindexer/src/lib/ponder-helpers.ts +++ b/apps/ensindexer/src/lib/ponder-helpers.ts @@ -11,7 +11,6 @@ import { Address, PublicClient } from "viem"; import * as z from "zod/v4"; import { ContractConfig } from "@ensnode/datasources"; -import { EnsRainbowApiClient } from "@ensnode/ensrainbow-sdk"; import type { BlockInfo, PonderStatus } from "@ensnode/ponder-metadata"; import { ENSIndexerConfig } from "@/config/types"; diff --git a/apps/ensindexer/src/lib/tokenscope/assets.ts b/apps/ensindexer/src/lib/tokenscope/assets.ts new file mode 100644 index 000000000..9f2f971ca --- /dev/null +++ b/apps/ensindexer/src/lib/tokenscope/assets.ts @@ -0,0 +1,14 @@ +/** + * An enum representing the possible CAIP-19 Asset Namespace values. + */ +export const AssetNamespaces = { + ERC721: "erc721", + ERC1155: "erc1155", +} as const; + +export type AssetNamespace = (typeof AssetNamespaces)[keyof typeof AssetNamespaces]; + +/** + * A uint256 value that identifies a specific token within an NFT contract. + */ +export type TokenId = bigint; diff --git a/apps/ensindexer/src/lib/tokenscope/sales.ts b/apps/ensindexer/src/lib/tokenscope/sales.ts index 97ebfd230..64c4e5535 100644 --- a/apps/ensindexer/src/lib/tokenscope/sales.ts +++ b/apps/ensindexer/src/lib/tokenscope/sales.ts @@ -1,29 +1,12 @@ -import { Price } from "@/lib/currencies"; -import { TokenId, TokenType } from "@/lib/tokenscope/tokens"; -import { ChainAddress, ChainId } from "@ensnode/datasources"; -import { Node, UnixTimestamp } from "@ensnode/ensnode-sdk"; +import { AccountId, Node } from "@ensnode/ensnode-sdk"; import { Address, Hex } from "viem"; -export interface OnchainEventRef { - /** - * Event.id set as the unique and deterministic identifier of the onchain event - * associated with the sale. - * - * Composite key format: "{chainId}-{blockNumber}-{logIndex}" (e.g., "1-1234567-5"). - * - * @example "1-1234567-5" - */ - eventId: string; - chainId: ChainId; - blockNumber: number; - logIndex: number; - timestamp: UnixTimestamp; - transactionHash: Hex; -} +import { Price } from "@/lib/currencies"; +import { AssetNamespace, TokenId } from "@/lib/tokenscope/assets"; export interface SupportedNFT { - tokenType: TokenType; - contract: ChainAddress; + assetNamespace: AssetNamespace; + contract: AccountId; tokenId: TokenId; domainId: Node; } @@ -33,7 +16,6 @@ export interface SupportedPayment { } export interface SupportedSale { - event: OnchainEventRef; orderHash: Hex; nft: SupportedNFT; payment: SupportedPayment; diff --git a/apps/ensindexer/src/lib/tokenscope/seaport.ts b/apps/ensindexer/src/lib/tokenscope/seaport.ts index df9d54d46..4e4c1ad07 100644 --- a/apps/ensindexer/src/lib/tokenscope/seaport.ts +++ b/apps/ensindexer/src/lib/tokenscope/seaport.ts @@ -1,11 +1,6 @@ import { CurrencyIds, getCurrencyIdForContract } from "@/lib/currencies"; -import { makeEventId } from "@/lib/ids"; -import { - OnchainEventRef, - SupportedNFT, - SupportedPayment, - SupportedSale, -} from "@/lib/tokenscope/sales"; +import { AssetNamespace, AssetNamespaces } from "@/lib/tokenscope/assets"; +import { SupportedNFT, SupportedPayment, SupportedSale } from "@/lib/tokenscope/sales"; import { SeaportConsiderationItem, SeaportItemType, @@ -13,24 +8,24 @@ import { SeaportOrderFulfilledEvent, } from "@/lib/tokenscope/seaport-types"; import { getKnownTokenIssuer } from "@/lib/tokenscope/token-issuers"; -import { TokenType, TokenTypes } from "@/lib/tokenscope/tokens"; -import { ChainId, ENSNamespaceId } from "@ensnode/datasources"; -import { uniq } from "@ensnode/ensnode-sdk"; +import { ENSNamespaceId } from "@ensnode/datasources"; +import { ChainId, uniq } from "@ensnode/ensnode-sdk"; /** - * Gets the supported TokenScope token type for a given Seaport item type. + * Gets the supported TokenScope Asset Namespace for a given Seaport ItemType. * * @param itemType - The Seaport item type to get the supported TokenScope token type for - * @returns the supported TokenScope token type for the given SeaPort item type, or null - * if the item type is not supported + * @returns the supported TokenScope Asset Namespace for the given Seaport ItemType, or null + * if the ItemType is not supported. */ -const getSupportedTokenType = (itemType: SeaportItemType): TokenType | null => { - if (itemType === SeaportItemType.ERC721) { - return TokenTypes.ERC721; - } else if (itemType === SeaportItemType.ERC1155) { - return TokenTypes.ERC1155; - } else { - return null; +const getAssetNamespace = (itemType: SeaportItemType): AssetNamespace | null => { + switch (itemType) { + case SeaportItemType.ERC721: + return AssetNamespaces.ERC721; + case SeaportItemType.ERC1155: + return AssetNamespaces.ERC1155; + default: + return null; } }; @@ -50,7 +45,7 @@ const getSupportedNFT = ( item: SeaportOfferItem | SeaportConsiderationItem, ): SupportedNFT | null => { // validate item as an ERC721/ERC1155 NFT - const tokenType = getSupportedTokenType(item.itemType); + const tokenType = getAssetNamespace(item.itemType); if (!tokenType) return null; // validate that the token is a known token issuing contract @@ -65,7 +60,7 @@ const getSupportedNFT = ( const domainId = tokenIssuer.getDomainId(tokenId); return { - tokenType, + assetNamespace: tokenType, contract, tokenId, domainId, @@ -77,13 +72,11 @@ const getSupportedPayment = ( chainId: ChainId, item: SeaportOfferItem | SeaportConsiderationItem, ): SupportedPayment | null => { - const currencyContract = { + // validate that the item is a supported currency + const currencyId = getCurrencyIdForContract(namespaceId, { chainId, address: item.token, - }; - - // validate that the item is a supported currency - const currencyId = getCurrencyIdForContract(namespaceId, currencyContract); + }); if (!currencyId) return null; // Unsupported currency @@ -177,35 +170,6 @@ const consolidateSupportedPayments = (payments: SupportedPayment[]): SupportedPa } satisfies SupportedPayment; }; -const buildOnchainEventRef = ( - chainId: ChainId, - event: SeaportOrderFulfilledEvent, -): OnchainEventRef => { - if (event.block.timestamp > BigInt(Number.MAX_SAFE_INTEGER)) { - throw new Error( - `Error building onchain event ref: block timestamp is too large: ${event.block.timestamp}`, - ); - } - - if (event.block.number > BigInt(Number.MAX_SAFE_INTEGER)) { - throw new Error( - `Error building onchain event ref: block number is too large: ${event.block.number}`, - ); - } - - const blockNumber = Number(event.block.number); - const timestamp = Number(event.block.timestamp); - - return { - eventId: makeEventId(chainId, event.block.number, event.log.logIndex), - chainId, - blockNumber, - logIndex: event.log.logIndex, - timestamp, - transactionHash: event.transaction.hash, - } satisfies OnchainEventRef; -}; - /** * Maps from Seaport-specific OrderFulfilled event into our more generic TokenScope `SupportedSale`, * if possible. TokenScope aims to deliver a simpler datamodel than Seaport provides but still @@ -244,7 +208,6 @@ export const getSupportedSaleFromOrderFulfilledEvent = ( !consolidatedConsiderationPayment ) { return { - event: buildOnchainEventRef(chainId, event), orderHash, nft: consolidatedOfferNFT, payment: consolidatedOfferPayment, @@ -262,7 +225,6 @@ export const getSupportedSaleFromOrderFulfilledEvent = ( consolidatedConsiderationPayment ) { return { - event: buildOnchainEventRef(chainId, event), orderHash, nft: consolidatedConsiderationNFT, payment: consolidatedConsiderationPayment, diff --git a/apps/ensindexer/src/lib/tokenscope/token-issuers.ts b/apps/ensindexer/src/lib/tokenscope/token-issuers.ts index 389c44c73..e131c63e3 100644 --- a/apps/ensindexer/src/lib/tokenscope/token-issuers.ts +++ b/apps/ensindexer/src/lib/tokenscope/token-issuers.ts @@ -1,17 +1,14 @@ -import { TokenId } from "@/lib/tokenscope/tokens"; -import { - ChainAddress, - DatasourceNames, - ENSNamespaceId, - isChainAddressEqual, - maybeGetDatasourceContractChainAddress, -} from "@ensnode/datasources"; +import { maybeGetDatasourceContract } from "@/lib/datasource-helpers"; +import { TokenId } from "@/lib/tokenscope/assets"; +import { DatasourceNames, ENSNamespaceId } from "@ensnode/datasources"; import { + AccountId, BASENAMES_NODE, ETH_NODE, LINEANAMES_NODE, LabelHash, type Node, + accountIdEqual, makeSubdomainNode, uint256ToHex32, } from "@ensnode/ensnode-sdk"; @@ -21,9 +18,9 @@ import { */ export interface TokenIssuer { /** - * The ChainAddress of the token issuer contract. + * The ContractAddress of the token issuer contract. */ - contract: ChainAddress; + contract: AccountId; /** * Applies the token issuer contract's logic for converting from the token id @@ -66,32 +63,32 @@ export const labelHashGeneratedTokenIdToNode = (tokenId: TokenId, parentNode: No * @returns an array of 0 or more known TokenIssuer for the specified namespace */ export const getKnownTokenIssuers = (namespaceId: ENSNamespaceId): TokenIssuer[] => { - const ethBaseRegistrar = maybeGetDatasourceContractChainAddress( + const ethBaseRegistrar = maybeGetDatasourceContract( namespaceId, DatasourceNames.ENSRoot, "BaseRegistrar", ); - const nameWrapper = maybeGetDatasourceContractChainAddress( + const nameWrapper = maybeGetDatasourceContract( namespaceId, DatasourceNames.ENSRoot, "NameWrapper", ); - const threeDnsBaseRegistrar = maybeGetDatasourceContractChainAddress( + const threeDnsBaseRegistrar = maybeGetDatasourceContract( namespaceId, DatasourceNames.ThreeDNSBase, "ThreeDNSToken", ); - const threeDnsOptimismRegistrar = maybeGetDatasourceContractChainAddress( + const threeDnsOptimismRegistrar = maybeGetDatasourceContract( namespaceId, DatasourceNames.ThreeDNSOptimism, "ThreeDNSToken", ); - const lineanamesRegistrar = maybeGetDatasourceContractChainAddress( + const lineanamesRegistrar = maybeGetDatasourceContract( namespaceId, DatasourceNames.Lineanames, "BaseRegistrar", ); - const basenamesRegistrar = maybeGetDatasourceContractChainAddress( + const basenamesRegistrar = maybeGetDatasourceContract( namespaceId, DatasourceNames.Basenames, "BaseRegistrar", @@ -167,28 +164,10 @@ export const getKnownTokenIssuers = (namespaceId: ENSNamespaceId): TokenIssuer[] */ export const getKnownTokenIssuer = ( namespaceId: ENSNamespaceId, - contract: ChainAddress, + contract: AccountId, ): TokenIssuer | null => { const tokenIssuers = getKnownTokenIssuers(namespaceId); - return ( - tokenIssuers.find((tokenIssuer) => isChainAddressEqual(tokenIssuer.contract, contract)) ?? null - ); -}; - -/** - * Identifies if the provided ChainAddress is a known token issuer. - * - * @param namespaceId - The ENSNamespace identifier (e.g. 'mainnet', 'sepolia', 'holesky', - * 'ens-test-env') - * @param chainAddress - The ChainAddress to check - * @returns a boolean indicating if the provided ChainAddress is a known token issuer in - * the specified namespace. - */ -export const isKnownTokenIssuer = ( - namespaceId: ENSNamespaceId, - chainAddress: ChainAddress, -): boolean => { - return getKnownTokenIssuer(namespaceId, chainAddress) !== null; + return tokenIssuers.find((tokenIssuer) => accountIdEqual(tokenIssuer.contract, contract)) ?? null; }; /** @@ -203,12 +182,12 @@ export const isKnownTokenIssuer = ( */ export function getDomainIdByTokenId( namespaceId: ENSNamespaceId, - contract: ChainAddress, + contract: AccountId, tokenId: TokenId, ): Node { const tokenIssuers = getKnownTokenIssuers(namespaceId); const tokenIssuer = tokenIssuers.find((tokenIssuer) => - isChainAddressEqual(tokenIssuer.contract, contract), + accountIdEqual(tokenIssuer.contract, contract), ); if (!tokenIssuer) { diff --git a/apps/ensindexer/src/lib/tokenscope/tokens.ts b/apps/ensindexer/src/lib/tokenscope/tokens.ts deleted file mode 100644 index e8146b503..000000000 --- a/apps/ensindexer/src/lib/tokenscope/tokens.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { ChainId } from "@ensnode/datasources"; -import { uint256ToHex32 } from "@ensnode/ensnode-sdk"; -import { Address } from "viem"; - -export const TokenTypes = { - ERC721: "ERC721", - ERC1155: "ERC1155", -} as const; - -export type TokenType = (typeof TokenTypes)[keyof typeof TokenTypes]; - -/** - * A uint256 value that identifies a specific token within a NFT contract. - */ -export type TokenId = bigint; - -/** - * Makes a unique and deterministic TokenRef. - * - * @example `${chainId}-${contractAddress}-${tokenId}` - * - * @param chainId - * @param contractAddress - * @param tokenId - * @returns a unique and deterministic TokenRef - */ -export const makeTokenRef = (chainId: ChainId, contractAddress: Address, tokenId: TokenId) => - `${chainId}-${contractAddress}-${uint256ToHex32(tokenId)}`; diff --git a/apps/ensindexer/src/plugins/tokenscope/handlers/Seaport.ts b/apps/ensindexer/src/plugins/tokenscope/handlers/Seaport.ts index df898a8bc..5e8f8d960 100644 --- a/apps/ensindexer/src/plugins/tokenscope/handlers/Seaport.ts +++ b/apps/ensindexer/src/plugins/tokenscope/handlers/Seaport.ts @@ -1,12 +1,13 @@ import { ponder } from "ponder:registry"; import schema from "ponder:schema"; -import { PluginName } from "@ensnode/ensnode-sdk"; +import { PluginName, uint256ToHex32 } from "@ensnode/ensnode-sdk"; +import { AssetId, ChainId } from "caip"; import config from "@/config"; import { upsertAccount } from "@/lib/db-helpers"; +import { makeEventId } from "@/lib/ids"; import { namespaceContract } from "@/lib/plugin-helpers"; import { getSupportedSaleFromOrderFulfilledEvent } from "@/lib/tokenscope/seaport"; -import { makeTokenRef } from "@/lib/tokenscope/tokens"; /** * Registers event handlers with Ponder. @@ -17,35 +18,56 @@ export default function () { ponder.on(namespaceContract(pluginName, "Seaport:OrderFulfilled"), async ({ context, event }) => { const sale = getSupportedSaleFromOrderFulfilledEvent(config.namespace, context.chain.id, event); + // TODO: remove these invariants and just store as bigint like god intended + if (event.block.timestamp > BigInt(Number.MAX_SAFE_INTEGER)) { + throw new Error( + `Error building onchain event ref: block timestamp is too large: ${event.block.timestamp}`, + ); + } + + if (event.block.number > BigInt(Number.MAX_SAFE_INTEGER)) { + throw new Error( + `Error building onchain event ref: block number is too large: ${event.block.number}`, + ); + } + + const blockNumber = Number(event.block.number); + const timestamp = Number(event.block.timestamp); + // no supported sale detected in event, no-op if (!sale) return; + console.log(sale); + // upsert buyer and seller accounts await upsertAccount(context, sale.seller); await upsertAccount(context, sale.buyer); // insert NameSale entity await context.db.insert(schema.nameSales).values({ - id: sale.event.eventId, + id: makeEventId(context.chain.id, event.block.number, event.log.logIndex), chainId: sale.nft.contract.chainId, - blockNumber: sale.event.blockNumber, - logIndex: sale.event.logIndex, - transactionHash: sale.event.transactionHash, + blockNumber, + logIndex: event.log.logIndex, + transactionHash: event.transaction.hash, orderHash: sale.orderHash, contractAddress: sale.nft.contract.address, tokenId: sale.nft.tokenId, - tokenType: sale.nft.tokenType, - tokenRef: makeTokenRef( - sale.nft.contract.chainId, - sale.nft.contract.address, - sale.nft.tokenId, - ), + assetNamespace: sale.nft.assetNamespace, + assetId: AssetId.format({ + chainId: ChainId.format({ + namespace: "eip155", + reference: sale.nft.contract.chainId.toString(), + }), + assetName: { namespace: sale.nft.assetNamespace, reference: sale.nft.contract.address }, + tokenId: uint256ToHex32(sale.nft.tokenId), + }), domainId: sale.nft.domainId, buyer: sale.buyer, seller: sale.seller, currency: sale.payment.price.currency, amount: sale.payment.price.amount, - timestamp: sale.event.timestamp, + timestamp, }); }); } diff --git a/packages/datasources/src/lib/types.ts b/packages/datasources/src/lib/types.ts index 0d6f3e99d..58325a6c3 100644 --- a/packages/datasources/src/lib/types.ts +++ b/packages/datasources/src/lib/types.ts @@ -121,30 +121,3 @@ export type ContractConfig = export type ENSNamespace = { [DatasourceNames.ENSRoot]: Datasource; } & Partial, Datasource>>; - -/** - * Chain ID - * - * Represents a unique identifier for a chain. - * Guaranteed to be a positive integer. - **/ -export type ChainId = number; - -/** - * Identifies a specific address on a specific chain. - */ -export interface ChainAddress { - chainId: ChainId; - address: Address; -} - -/** - * Identifies if the provided ChainAddress values are equal. - * - * @param ca1 - The first ChainAddress to compare - * @param ca2 - The second ChainAddress to compare - * @returns a boolean indicating if the provided ChainAddress values are equal - */ -export const isChainAddressEqual = (ca1: ChainAddress, ca2: ChainAddress): boolean => { - return ca1.chainId === ca2.chainId && isAddressEqual(ca1.address, ca2.address); -}; diff --git a/packages/datasources/src/namespaces.ts b/packages/datasources/src/namespaces.ts index 994aff9e6..7b662aff5 100644 --- a/packages/datasources/src/namespaces.ts +++ b/packages/datasources/src/namespaces.ts @@ -1,8 +1,6 @@ import ensTestEnv from "./ens-test-env"; import holesky from "./holesky"; import { - ChainAddress, - ChainId, Datasource, DatasourceName, DatasourceNames, @@ -72,35 +70,6 @@ export const maybeGetDatasource = ( datasourceName: DatasourceName, ): Datasource | undefined => (getENSNamespace(namespaceId) as ENSNamespace)[datasourceName]; -/** - * Gets the chain address for the specified namespace, datasource, and - * contract name, or undefined if it is not defined or is not a single chain address. - * - * This is useful when you want to retrieve a single ChainAddress for an arbitrary contract - * where it may or may not actually be defined. - * - * @param namespaceId - The ENSNamespace identifier (e.g. 'mainnet', 'sepolia', 'holesky', - * 'ens-test-env') - * @param datasourceName - The name of the Datasource to search for contractName in - * @param contractName - The name of the contract to retrieve the chain address for - * @returns The ChainAddress for the given namespace, datasource, and contract - * name, or undefined if it does not exist or is not a single chain address - */ -export const maybeGetDatasourceContractChainAddress = ( - namespaceId: ENSNamespaceId, - datasourceName: DatasourceName, - contractName: string, -): ChainAddress | undefined => { - const datasource = maybeGetDatasource(namespaceId, datasourceName); - if (!datasource) return undefined; - const maybeAddress = datasource.contracts[contractName]?.address; - if (maybeAddress === undefined || Array.isArray(maybeAddress)) return undefined; - return { - chainId: datasource.chain.id, - address: maybeAddress, - } satisfies ChainAddress; -}; - /** * Returns the chain for the ENS Root Datasource within the selected namespace. * @@ -115,17 +84,3 @@ export const getENSRootChain = (namespaceId: ENSNamespaceId) => * @returns the chain ID that hosts the ENS Root */ export const getENSRootChainId = (namespaceId: ENSNamespaceId) => getENSRootChain(namespaceId).id; - -/** - * Gets all the distinct chainIds with datasources in the specified namespace. - * Note: This takes no consideration of which datasources are configured for indexing. - * - * @param namespaceId - The ENSNamespace identifier (e.g. 'mainnet', 'sepolia', 'holesky', - * 'ens-test-env') - * @returns an array of distinct chainIds with datasources in the specified namespace - */ -export const getChainIdsForNamespace = (namespaceId: ENSNamespaceId): ChainId[] => { - const namespace = getENSNamespace(namespaceId); - const chainIds = Object.values(namespace).map((datasource) => datasource.chain.id); - return [...new Set(chainIds)]; -}; diff --git a/packages/ensnode-schema/src/tokenscope.schema.ts b/packages/ensnode-schema/src/tokenscope.schema.ts index 2ab5ab1e8..3ac2974e9 100644 --- a/packages/ensnode-schema/src/tokenscope.schema.ts +++ b/packages/ensnode-schema/src/tokenscope.schema.ts @@ -53,16 +53,19 @@ export const nameSales = onchainTable( tokenId: t.bigint().notNull(), /** - * The type of token that was sold (ERC721 or ERC1155). + * The CAIP-19 Asset Namespace of the token that was sold. Either `erc721` or `erc1155`. + * + * @see https://chainagnostic.org/CAIPs/caip-19 */ - tokenType: t.text().notNull(), + assetNamespace: t.text().notNull(), /** - * Unique and deterministic identifier of token that was sold. + * The CAIP-19 Asset ID of token that was sold. This is a globally unique reference to the + * specific asset in question. * - * Composite key format: "{chainId}-{contractAddress}-{tokenId}" + * @see https://chainagnostic.org/CAIPs/caip-19 */ - tokenRef: t.text().notNull(), + assetId: t.text().notNull(), /** * The namehash of the ENS domain that was sold. @@ -105,7 +108,7 @@ export const nameSales = onchainTable( }), (t) => ({ idx_domainId: index().on(t.domainId), - idx_tokenRef: index().on(t.tokenRef), + idx_assetId: index().on(t.assetId), idx_buyer: index().on(t.buyer), idx_seller: index().on(t.seller), idx_timestamp: index().on(t.timestamp), diff --git a/packages/ensnode-sdk/src/ensindexer/config/types.ts b/packages/ensnode-sdk/src/ensindexer/config/types.ts index afeb06eb2..d991ae5d8 100644 --- a/packages/ensnode-sdk/src/ensindexer/config/types.ts +++ b/packages/ensnode-sdk/src/ensindexer/config/types.ts @@ -1,10 +1,7 @@ -import type { ChainId, ENSNamespaceId } from "@ensnode/datasources"; -import type { - EnsRainbowClientLabelSet, - EnsRainbowServerLabelSet, - LabelSetId, - LabelSetVersion, -} from "../../ensrainbow"; +import type { ENSNamespaceId } from "@ensnode/datasources"; + +import type { EnsRainbowClientLabelSet } from "../../ensrainbow"; +import { ChainId } from "../../shared"; /** * A PluginName is a unique id for a 'plugin': we use the notion of diff --git a/packages/ensnode-sdk/src/shared/account-id.ts b/packages/ensnode-sdk/src/shared/account-id.ts new file mode 100644 index 000000000..29508f82f --- /dev/null +++ b/packages/ensnode-sdk/src/shared/account-id.ts @@ -0,0 +1,9 @@ +import { AccountId } from "@ensnode/ensnode-sdk"; +import { isAddressEqual } from "viem"; + +/** + * Determines where the provided AccountId values represent the same address on the same chain. + */ +export const accountIdEqual = (a: AccountId, b: AccountId): boolean => { + return a.chainId === b.chainId && isAddressEqual(a.address, b.address); +}; diff --git a/packages/ensnode-sdk/src/shared/index.ts b/packages/ensnode-sdk/src/shared/index.ts index 82d06500e..8a15fed28 100644 --- a/packages/ensnode-sdk/src/shared/index.ts +++ b/packages/ensnode-sdk/src/shared/index.ts @@ -13,3 +13,4 @@ export { deserializeUrl, } from "./deserialize"; export * from "./is-normalized"; +export * from "./account-id"; diff --git a/packages/ensnode-sdk/src/shared/types.ts b/packages/ensnode-sdk/src/shared/types.ts index e7c13b8e4..1be985e58 100644 --- a/packages/ensnode-sdk/src/shared/types.ts +++ b/packages/ensnode-sdk/src/shared/types.ts @@ -1,7 +1,22 @@ -import { ChainId } from "@ensnode/datasources"; +import { Address } from "viem"; -// re-export ChainId for backwards compatibility -export { type ChainId }; +/** + * Chain ID + * + * Represents a unique identifier for a chain. + * Guaranteed to be a positive integer. + **/ +export type ChainId = number; + +/** + * Represents an account (contract or EOA) at `address` on chain `chainId`. + * + * @see https://chainagnostic.org/CAIPs/caip-10 + */ +export interface AccountId { + chainId: ChainId; + address: Address; +} /** * Block Number diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 144ce1143..e8362beee 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -6,15 +6,45 @@ settings: catalogs: default: + '@astrojs/react': + specifier: ^4.2.0 + version: 4.2.0 + '@astrojs/tailwind': + specifier: ^6.0.0 + version: 6.0.0 '@biomejs/biome': specifier: ^1.9.4 version: 1.9.4 + '@namehash/namekit-react': + specifier: 0.12.0 + version: 0.12.0 '@ponder/utils': specifier: 0.2.10 version: 0.2.10 '@types/node': specifier: ^22.14.0 version: 22.15.3 + '@vitest/coverage-v8': + specifier: ^3.1.1 + version: 3.1.1 + astro-font: + specifier: ^1.0.0 + version: 1.0.0 + astro-seo: + specifier: ^0.8.4 + version: 0.8.4 + date-fns: + specifier: ^4.1.0 + version: 4.1.0 + drizzle-orm: + specifier: '=0.41.0' + version: 0.41.0 + hono: + specifier: ^4.7.6 + version: 4.7.8 + ponder: + specifier: 0.11.43 + version: 0.11.43 tsup: specifier: ^8.3.6 version: 8.3.6 @@ -24,6 +54,12 @@ catalogs: viem: specifier: ^2.22.13 version: 2.23.2 + vitest: + specifier: ^3.1.1 + version: 3.1.1 + zod: + specifier: ^3.25.7 + version: 3.25.7 overrides: '@graphiql/react>@headlessui/react': 2.2.0 @@ -252,9 +288,6 @@ importers: '@hono/zod-validator': specifier: ^0.7.2 version: 0.7.2(hono@4.7.8)(zod@3.25.7) - '@opensea/seaport-js': - specifier: ^4.0.5 - version: 4.0.5(bufferutil@4.0.9)(utf-8-validate@5.0.10) '@opentelemetry/api': specifier: ^1.9.0 version: 1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a) @@ -288,6 +321,9 @@ importers: '@types/dns-packet': specifier: ^5.6.5 version: 5.6.5 + caip: + specifier: ^1.1.1 + version: 1.1.1 date-fns: specifier: 'catalog:' version: 4.1.0 @@ -1937,9 +1973,6 @@ packages: cpu: [x64] os: [win32] - '@noble/curves@1.2.0': - resolution: {integrity: sha512-oYclrNgRaM9SsBUBVbb8M6DTV7ZHRTKugureoYEncY5c65HOmRzvSiTE3y5CYaPYJA/GVkrhXEoF0M3Ya9PMnw==} - '@noble/curves@1.4.0': resolution: {integrity: sha512-p+4cb332SFCrReJkCYe8Xzm0OWi4Jji5jVdIZRL/PmacmDkFNw6MrrV+gGpiPxLHbV+zKFRywUWbaseT+tZRXg==} @@ -1950,10 +1983,6 @@ packages: resolution: {integrity: sha512-warwspo+UYUPep0Q+vtdVB4Ugn8GGQj8iyB3gnRWsztmUHTI3S1nhdiWNsPUGL0vud7JlRRk1XEu7Lq1KGTnMQ==} engines: {node: ^14.21.3 || >=16} - '@noble/hashes@1.3.2': - resolution: {integrity: sha512-MVC8EAQp7MvEcm30KWENFjgR+Mkmf+D189XJTkFIlwohU5hcBbn1ZkKq7KVTi2Hme3PMGF390DaL52beVrIihQ==} - engines: {node: '>= 16'} - '@noble/hashes@1.4.0': resolution: {integrity: sha512-V1JJ1WTRUqHHrOSh597hURcMqVKVGL/ea3kv0gSnEdsEZ0/+VyPghM1lMNGc00z7CIQorSvbKpuJkxvuHbvdbg==} engines: {node: '>= 16'} @@ -2034,10 +2063,6 @@ packages: '@octokit/types@13.10.0': resolution: {integrity: sha512-ifLaO34EbbPj0Xgro4G5lP5asESjwHracYJvVaPIyXMuiuXLlhic3S47cBdTb+jfODkTE5YtGCLt3Ay3+J97sA==} - '@opensea/seaport-js@4.0.5': - resolution: {integrity: sha512-hyJEHSCFmO7kv2G+ima0kCpt0kvLa6QOSHb1HJuLd8DS3bao0gOa/Q3AhM3xUqO6SZZ8aD9njhu1EDqjC/5pOw==} - engines: {node: '>=20.0.0'} - '@opentelemetry/api-logs@0.202.0': resolution: {integrity: sha512-fTBjMqKCfotFWfLzaKyhjLvyEyq5vDKTTFfBmx21btv3gvy8Lq6N5Dh2OzqeuN4DjtpSvNT1uNVfg08eD2Rfxw==} engines: {node: '>=8.0.0'} @@ -3451,9 +3476,6 @@ packages: '@types/node@22.15.3': resolution: {integrity: sha512-lX7HFZeHf4QG/J7tBZqrCAXwz9J5RD56Y6MpP0eJkka8p+K0RY/yBTW7CYFJ4VGCclxqOLKmiGP5juQc6MKgcw==} - '@types/node@22.7.5': - resolution: {integrity: sha512-jML7s2NAzMWc//QSJ1a3prpk78cOPchGvXJsC3C6R6PSMoooztvRVQEz89gmBTBY1SPMaqo5teB4uNHPdetShQ==} - '@types/progress@2.0.7': resolution: {integrity: sha512-iadjw02vte8qWx7U0YM++EybBha2CQLPGu9iJ97whVgJUT5Zq9MjAPYUnbfRI2Kpehimf1QjFJYxD0t8nqzu5w==} @@ -3717,9 +3739,6 @@ packages: engines: {node: '>=0.4.0'} hasBin: true - aes-js@4.0.0-beta.5: - resolution: {integrity: sha512-G965FqalsNyrPqgEGON7nIx1e/OVENSgiEIzyC63haUMuvNnwIgIjMs52hlTCKhkBny7A2ORNlfY9Zu+jmGk1Q==} - agent-base@7.1.4: resolution: {integrity: sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==} engines: {node: '>= 14'} @@ -3984,9 +4003,6 @@ packages: buffer-crc32@0.2.13: resolution: {integrity: sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==} - buffer-reverse@1.0.1: - resolution: {integrity: sha512-M87YIUBsZ6N924W57vDwT/aOu8hw7ZgdByz6ijksLjmHJELBASmYTTlNHRgjE+pTsT9oJXGaDSgqqwfdHotDUg==} - buffer-writer@2.0.0: resolution: {integrity: sha512-a7ZpuTZU1TRtnwyCNW3I5dc0wWNC3VR9S++Ewyk2HHZdrO3CQJqSpd+95Us590V6AL7JqUAH2IwZ/398PmNFgw==} engines: {node: '>=4'} @@ -4012,6 +4028,9 @@ packages: resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==} engines: {node: '>=8'} + caip@1.1.1: + resolution: {integrity: sha512-a3v5lteUUOoyRI0U6qe5ayCCGkF2mCmJ5zQMDnOD2vRjgRg6sm9p8TsRC2h4D4beyqRN9RYniphAPnj/+jQC6g==} + call-bind-apply-helpers@1.0.1: resolution: {integrity: sha512-BhYE+WDaywFg2TBWYNXAE+8B1ATnThNBqXHP5nQu0jWJdVvY2hvkpyB3qOmtmDePiS5/BDQ8wASEWGMWRG148g==} engines: {node: '>= 0.4'} @@ -4272,9 +4291,6 @@ packages: crossws@0.3.3: resolution: {integrity: sha512-/71DJT3xJlqSnBr83uGJesmVHSzZEvgxHt/fIKxBAAngqMHmnBWQNxCphVxxJ2XL3xleu5+hJD6IQ3TglBedcw==} - crypto-js@4.2.0: - resolution: {integrity: sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q==} - css-select@5.1.0: resolution: {integrity: sha512-nwoRF1rvRRnnCqqY7updORDsuqKzqYJ28+oSMaJMMgOauh3fvwHqMS7EZpIPqK8GL+g9mKxF1vP/ZjSeNjEVHg==} @@ -5046,10 +5062,6 @@ packages: resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} engines: {node: '>=0.10.0'} - ethers@6.15.0: - resolution: {integrity: sha512-Kf/3ZW54L4UT0pZtsY/rf+EkBU7Qi5nnhonjUb8yTXcxH3cdcWrV2cRyk0Xk/4jK6OoHhxxZHriyhje20If2hQ==} - engines: {node: '>=14.0.0'} - event-target-shim@5.0.1: resolution: {integrity: sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==} engines: {node: '>=6'} @@ -6205,10 +6217,6 @@ packages: resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} engines: {node: '>= 8'} - merkletreejs@0.5.2: - resolution: {integrity: sha512-MHqclSWRSQQbYciUMALC3PZmE23NPf5IIYo+Z7qAz5jVcqgCB95L1T9jGcr+FtOj2Pa2/X26uG2Xzxs7FJccUg==} - engines: {node: '>= 7.6.0'} - mermaid@11.10.0: resolution: {integrity: sha512-oQsFzPBy9xlpnGxUqLbVY8pvknLlsNIJ0NWwi8SUJjhbP1IT0E0o1lfhU4iYV3ubpy+xkzkaOyDUQMn06vQElQ==} @@ -7719,10 +7727,6 @@ packages: resolution: {integrity: sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==} hasBin: true - treeify@1.1.0: - resolution: {integrity: sha512-1m4RA7xVAJrSGrrXGs0L3YTwyvBs2S8PbRHaLZAkFw7JR8oIFwYtysxlBZhYIa7xSyiYJKZ3iGrrk55cGA3i9A==} - engines: {node: '>=0.6'} - trim-lines@3.0.1: resolution: {integrity: sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==} @@ -7775,9 +7779,6 @@ packages: tsconfig-paths@3.15.0: resolution: {integrity: sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg==} - tslib@2.7.0: - resolution: {integrity: sha512-gLXCKdN1/j47AiHiOkJN69hJmcbGTHI0ImLmbYLHykhgeN0jVGola9yVjFgzCUklsZQMW55o+dW7IXv3RCXDzA==} - tslib@2.8.1: resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} @@ -8584,7 +8585,7 @@ snapshots: dependencies: '@astrojs/compiler': 2.12.2 '@astrojs/yaml2ts': 0.2.2 - '@jridgewell/sourcemap-codec': 1.5.0 + '@jridgewell/sourcemap-codec': 1.5.5 '@volar/kit': 2.4.11(typescript@5.7.3) '@volar/language-core': 2.4.11 '@volar/language-server': 2.4.11 @@ -8772,7 +8773,7 @@ snapshots: '@babel/core@7.26.9': dependencies: '@ampproject/remapping': 2.3.0 - '@babel/code-frame': 7.26.2 + '@babel/code-frame': 7.27.1 '@babel/generator': 7.27.0 '@babel/helper-compilation-targets': 7.26.5 '@babel/helper-module-transforms': 7.26.0(@babel/core@7.26.9) @@ -8782,7 +8783,7 @@ snapshots: '@babel/traverse': 7.27.0 '@babel/types': 7.27.0 convert-source-map: 2.0.0 - debug: 4.4.0 + debug: 4.4.1 gensync: 1.0.0-beta.2 json5: 2.2.3 semver: 6.3.1 @@ -8816,7 +8817,7 @@ snapshots: dependencies: '@babel/core': 7.26.9 '@babel/helper-module-imports': 7.25.9 - '@babel/helper-validator-identifier': 7.25.9 + '@babel/helper-validator-identifier': 7.27.1 '@babel/traverse': 7.27.0 transitivePeerDependencies: - supports-color @@ -8858,18 +8859,18 @@ snapshots: '@babel/template@7.27.0': dependencies: - '@babel/code-frame': 7.26.2 + '@babel/code-frame': 7.27.1 '@babel/parser': 7.27.0 '@babel/types': 7.27.0 '@babel/traverse@7.27.0': dependencies: - '@babel/code-frame': 7.26.2 + '@babel/code-frame': 7.27.1 '@babel/generator': 7.27.0 '@babel/parser': 7.27.0 '@babel/template': 7.27.0 '@babel/types': 7.27.0 - debug: 4.4.0 + debug: 4.4.1 globals: 11.12.0 transitivePeerDependencies: - supports-color @@ -9799,13 +9800,12 @@ snapshots: '@jridgewell/sourcemap-codec@1.5.0': {} - '@jridgewell/sourcemap-codec@1.5.5': - optional: true + '@jridgewell/sourcemap-codec@1.5.5': {} '@jridgewell/trace-mapping@0.3.25': dependencies: '@jridgewell/resolve-uri': 3.1.2 - '@jridgewell/sourcemap-codec': 1.5.0 + '@jridgewell/sourcemap-codec': 1.5.5 '@jridgewell/trace-mapping@0.3.9': dependencies: @@ -9993,10 +9993,6 @@ snapshots: '@next/swc-win32-x64-msvc@15.2.4': optional: true - '@noble/curves@1.2.0': - dependencies: - '@noble/hashes': 1.3.2 - '@noble/curves@1.4.0': dependencies: '@noble/hashes': 1.4.0 @@ -10009,8 +10005,6 @@ snapshots: dependencies: '@noble/hashes': 1.7.1 - '@noble/hashes@1.3.2': {} - '@noble/hashes@1.4.0': {} '@noble/hashes@1.5.0': {} @@ -10094,14 +10088,6 @@ snapshots: dependencies: '@octokit/openapi-types': 24.2.0 - '@opensea/seaport-js@4.0.5(bufferutil@4.0.9)(utf-8-validate@5.0.10)': - dependencies: - ethers: 6.15.0(bufferutil@4.0.9)(utf-8-validate@5.0.10) - merkletreejs: 0.5.2 - transitivePeerDependencies: - - bufferutil - - utf-8-validate - '@opentelemetry/api-logs@0.202.0': dependencies: '@opentelemetry/api': 1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a) @@ -11522,10 +11508,6 @@ snapshots: dependencies: undici-types: 6.21.0 - '@types/node@22.7.5': - dependencies: - undici-types: 6.19.8 - '@types/progress@2.0.7': dependencies: '@types/node': 22.15.3 @@ -11623,7 +11605,7 @@ snapshots: dependencies: '@typescript-eslint/types': 8.24.0 '@typescript-eslint/visitor-keys': 8.24.0 - debug: 4.4.0 + debug: 4.4.1 fast-glob: 3.3.3 is-glob: 4.0.3 minimatch: 9.0.5 @@ -11666,7 +11648,7 @@ snapshots: dependencies: '@ampproject/remapping': 2.3.0 '@bcoe/v8-coverage': 1.0.2 - debug: 4.4.0 + debug: 4.4.1 istanbul-lib-coverage: 3.2.2 istanbul-lib-report: 3.0.1 istanbul-lib-source-maps: 5.0.6 @@ -11684,7 +11666,7 @@ snapshots: dependencies: '@ampproject/remapping': 2.3.0 '@bcoe/v8-coverage': 1.0.2 - debug: 4.4.0 + debug: 4.4.1 istanbul-lib-coverage: 3.2.2 istanbul-lib-report: 3.0.1 istanbul-lib-source-maps: 5.0.6 @@ -11900,8 +11882,6 @@ snapshots: acorn@8.15.0: {} - aes-js@4.0.0-beta.5: {} - agent-base@7.1.4: {} agentkeepalive@4.6.0: @@ -12290,8 +12270,6 @@ snapshots: buffer-crc32@0.2.13: {} - buffer-reverse@1.0.1: {} - buffer-writer@2.0.0: {} buffer@6.0.3: @@ -12315,6 +12293,8 @@ snapshots: cac@6.7.14: {} + caip@1.1.1: {} + call-bind-apply-helpers@1.0.1: dependencies: es-errors: 1.3.0 @@ -12584,8 +12564,6 @@ snapshots: dependencies: uncrypto: 0.1.3 - crypto-js@4.2.0: {} - css-select@5.1.0: dependencies: boolbase: 1.0.0 @@ -13434,19 +13412,6 @@ snapshots: esutils@2.0.3: {} - ethers@6.15.0(bufferutil@4.0.9)(utf-8-validate@5.0.10): - dependencies: - '@adraffy/ens-normalize': 1.10.1 - '@noble/curves': 1.2.0 - '@noble/hashes': 1.3.2 - '@types/node': 22.7.5 - aes-js: 4.0.0-beta.5 - tslib: 2.7.0 - ws: 8.17.1(bufferutil@4.0.9)(utf-8-validate@5.0.10) - transitivePeerDependencies: - - bufferutil - - utf-8-validate - event-target-shim@5.0.1: {} eventemitter3@5.0.1: {} @@ -13482,7 +13447,7 @@ snapshots: extract-zip@2.0.1: dependencies: - debug: 4.4.0 + debug: 4.4.1 get-stream: 5.2.0 yauzl: 2.10.0 optionalDependencies: @@ -14374,7 +14339,7 @@ snapshots: istanbul-lib-source-maps@5.0.6: dependencies: '@jridgewell/trace-mapping': 0.3.25 - debug: 4.4.0 + debug: 4.4.1 istanbul-lib-coverage: 3.2.2 transitivePeerDependencies: - supports-color @@ -14867,12 +14832,6 @@ snapshots: merge2@1.4.1: {} - merkletreejs@0.5.2: - dependencies: - buffer-reverse: 1.0.1 - crypto-js: 4.2.0 - treeify: 1.1.0 - mermaid@11.10.0: dependencies: '@braintree/sanitize-url': 7.1.1 @@ -16842,8 +16801,6 @@ snapshots: tree-kill@1.2.2: {} - treeify@1.1.0: {} - trim-lines@3.0.1: {} trim-trailing-lines@2.1.0: {} @@ -16890,8 +16847,6 @@ snapshots: minimist: 1.2.8 strip-bom: 3.0.0 - tslib@2.7.0: {} - tslib@2.8.1: {} tsup@8.3.6(jiti@2.4.2)(postcss@8.5.3)(tsx@4.19.3)(typescript@5.7.3)(yaml@2.7.0): From 084ad4daa5a42113b21217034acd890be91abe5f Mon Sep 17 00:00:00 2001 From: shrugs Date: Mon, 25 Aug 2025 16:59:36 -0500 Subject: [PATCH 38/61] fix: remove console log --- apps/ensindexer/src/plugins/tokenscope/handlers/Seaport.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/apps/ensindexer/src/plugins/tokenscope/handlers/Seaport.ts b/apps/ensindexer/src/plugins/tokenscope/handlers/Seaport.ts index 5e8f8d960..89292fb23 100644 --- a/apps/ensindexer/src/plugins/tokenscope/handlers/Seaport.ts +++ b/apps/ensindexer/src/plugins/tokenscope/handlers/Seaport.ts @@ -37,8 +37,6 @@ export default function () { // no supported sale detected in event, no-op if (!sale) return; - console.log(sale); - // upsert buyer and seller accounts await upsertAccount(context, sale.seller); await upsertAccount(context, sale.buyer); From 13e05f31b5bea408d77bdec0b19053254a1f3abb Mon Sep 17 00:00:00 2001 From: shrugs Date: Mon, 25 Aug 2025 17:04:22 -0500 Subject: [PATCH 39/61] fix: type referential error --- apps/ensadmin/src/lib/chains.ts | 2 +- packages/ensnode-sdk/src/ensindexer/config/types.ts | 2 +- packages/ensnode-sdk/src/shared/account-id.ts | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/ensadmin/src/lib/chains.ts b/apps/ensadmin/src/lib/chains.ts index 00a61dc4c..6fef0f9e7 100644 --- a/apps/ensadmin/src/lib/chains.ts +++ b/apps/ensadmin/src/lib/chains.ts @@ -1,4 +1,4 @@ -import { Datasource, type ENSNamespaceId, getENSNamespace } from "@ensnode/datasources"; +import { type Datasource, type ENSNamespaceId, getENSNamespace } from "@ensnode/datasources"; import { type Chain } from "viem"; import { anvil } from "viem/chains"; diff --git a/packages/ensnode-sdk/src/ensindexer/config/types.ts b/packages/ensnode-sdk/src/ensindexer/config/types.ts index d991ae5d8..6ce9d8287 100644 --- a/packages/ensnode-sdk/src/ensindexer/config/types.ts +++ b/packages/ensnode-sdk/src/ensindexer/config/types.ts @@ -1,4 +1,4 @@ -import type { ENSNamespaceId } from "@ensnode/datasources"; +import { ENSNamespaceId } from "@ensnode/datasources"; import type { EnsRainbowClientLabelSet } from "../../ensrainbow"; import { ChainId } from "../../shared"; diff --git a/packages/ensnode-sdk/src/shared/account-id.ts b/packages/ensnode-sdk/src/shared/account-id.ts index 29508f82f..cdafc2a42 100644 --- a/packages/ensnode-sdk/src/shared/account-id.ts +++ b/packages/ensnode-sdk/src/shared/account-id.ts @@ -1,5 +1,5 @@ -import { AccountId } from "@ensnode/ensnode-sdk"; import { isAddressEqual } from "viem"; +import { AccountId } from "./types"; /** * Determines where the provided AccountId values represent the same address on the same chain. From 0626cb61ac5339545b4919019bd57bafcd1e8ff3 Mon Sep 17 00:00:00 2001 From: shrugs Date: Mon, 25 Aug 2025 17:06:51 -0500 Subject: [PATCH 40/61] remove redundant satisfies operators --- apps/ensindexer/src/lib/tokenscope/seaport.ts | 25 +++++++++---------- 1 file changed, 12 insertions(+), 13 deletions(-) diff --git a/apps/ensindexer/src/lib/tokenscope/seaport.ts b/apps/ensindexer/src/lib/tokenscope/seaport.ts index 4e4c1ad07..f1e9429b4 100644 --- a/apps/ensindexer/src/lib/tokenscope/seaport.ts +++ b/apps/ensindexer/src/lib/tokenscope/seaport.ts @@ -64,7 +64,7 @@ const getSupportedNFT = ( contract, tokenId, domainId, - } satisfies SupportedNFT; + }; }; const getSupportedPayment = ( @@ -101,7 +101,7 @@ const getSupportedPayment = ( currency: currencyId, amount: item.amount, }, - } satisfies SupportedPayment; + }; }; interface SeaportItemExtractions { @@ -123,26 +123,25 @@ const getSeaportItemExtractions = ( chainId: ChainId, items: readonly (SeaportOfferItem | SeaportConsiderationItem)[], ): SeaportItemExtractions => { - let nfts: SupportedNFT[] = []; - let payments: SupportedPayment[] = []; + const extractions: SeaportItemExtractions = { + nfts: [], + payments: [], + }; // each item is either a supported NFT, a supported payment, or unsupported for (const item of items) { const nft = getSupportedNFT(namespaceId, chainId, item); if (nft) { - nfts.push(nft); + extractions.nfts.push(nft); } else { const payment = getSupportedPayment(namespaceId, chainId, item); if (payment) { - payments.push(payment); + extractions.payments.push(payment); } } } - return { - nfts, - payments, - } satisfies SeaportItemExtractions; + return extractions; }; const consolidateSupportedNFTs = (nfts: SupportedNFT[]): SupportedNFT | null => { @@ -167,7 +166,7 @@ const consolidateSupportedPayments = (payments: SupportedPayment[]): SupportedPa currency: uniqueCurrencies[0]!, // we verified above there's exactly one currency amount: totalAmount, }, - } satisfies SupportedPayment; + }; }; /** @@ -213,7 +212,7 @@ export const getSupportedSaleFromOrderFulfilledEvent = ( payment: consolidatedOfferPayment, seller: offerer, buyer: recipient, - } satisfies SupportedSale; + }; } // consideration is exactly 1 supported NFT and offer consolidates to 1 supported payment @@ -230,7 +229,7 @@ export const getSupportedSaleFromOrderFulfilledEvent = ( payment: consolidatedConsiderationPayment, seller: recipient, buyer: offerer, - } satisfies SupportedSale; + }; } // otherwise, unsupported sale From 66d6a4cd1cdefb9a147a6a2c908454616736fc4b Mon Sep 17 00:00:00 2001 From: "lightwalker.eth" <126201998+lightwalker-eth@users.noreply.github.com> Date: Tue, 26 Aug 2025 11:13:32 +0300 Subject: [PATCH 41/61] Refine docs --- apps/ensindexer/src/lib/datasource-helpers.ts | 12 ++++++------ apps/ensindexer/src/lib/tokenscope/token-issuers.ts | 12 ++++++------ 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/apps/ensindexer/src/lib/datasource-helpers.ts b/apps/ensindexer/src/lib/datasource-helpers.ts index 9fab29936..e0489d096 100644 --- a/apps/ensindexer/src/lib/datasource-helpers.ts +++ b/apps/ensindexer/src/lib/datasource-helpers.ts @@ -22,18 +22,18 @@ export const getChainIdsInNamespace = (namespaceId: ENSNamespaceId): ChainId[] = }; /** - * Gets the chain address for the specified namespace, datasource, and - * contract name, or undefined if it is not defined or is not a single chain address. + * Gets the AccountId of the requested contract for the specified namespace, datasource, and + * contract name, or undefined if it is not defined or is not a single AccountId. * - * This is useful when you want to retrieve a single ChainAddress for an arbitrary contract + * This is useful when you want to retrieve a single AccountId for an arbitrary contract * where it may or may not actually be defined. * * @param namespaceId - The ENSNamespace identifier (e.g. 'mainnet', 'sepolia', 'holesky', * 'ens-test-env') * @param datasourceName - The name of the Datasource to search for contractName in - * @param contractName - The name of the contract to retrieve the chain address for - * @returns The ChainAddress for the given namespace, datasource, and contract - * name, or undefined if it does not exist or is not a single chain address + * @param contractName - The name of the contract to retrieve the AccountId for + * @returns The AccountId of the requested contract for the given namespace, datasource, + * and contract name, or undefined if it does not exist or is not a single AccountId */ export const maybeGetDatasourceContract = ( namespaceId: ENSNamespaceId, diff --git a/apps/ensindexer/src/lib/tokenscope/token-issuers.ts b/apps/ensindexer/src/lib/tokenscope/token-issuers.ts index e131c63e3..d5550eb91 100644 --- a/apps/ensindexer/src/lib/tokenscope/token-issuers.ts +++ b/apps/ensindexer/src/lib/tokenscope/token-issuers.ts @@ -18,7 +18,7 @@ import { */ export interface TokenIssuer { /** - * The ContractAddress of the token issuer contract. + * The AccountId of the token issuer contract. */ contract: AccountId; @@ -154,13 +154,13 @@ export const getKnownTokenIssuers = (namespaceId: ENSNamespaceId): TokenIssuer[] }; /** - * Gets the known token issuer for the given ChainAddress in the specified namespace. + * Gets the known token issuer for the given contract in the specified namespace. * * @param namespaceId - The ENSNamespace identifier (e.g. 'mainnet', 'sepolia', 'holesky', * 'ens-test-env') - * @param contract - The ChainAddress to get the known token issuer for - * @returns the known token issuer for the given ChainAddress, or null - * if the ChainAddress is not a known token issuer in the specified namespace + * @param contract - The AccountId of the contract to get the known token issuer for + * @returns the known token issuer for the given contract, or null + * if the contract is not a known token issuer in the specified namespace */ export const getKnownTokenIssuer = ( namespaceId: ENSNamespaceId, @@ -175,7 +175,7 @@ export const getKnownTokenIssuer = ( * * @param namespaceId - The ENSNamespace identifier (e.g. 'mainnet', 'sepolia', 'holesky', * 'ens-test-env') - * @param contract - The ChainAddress of the NFT contract + * @param contract - The AccountId of the NFT contract * @param tokenId - The tokenId of the NFT * @returns the domainId (Node) for ENS name associated with the NFT * @throws an error if the contract is not a known token issuing contract in the specified namespace From 6fb84b208d4cae84c67440070022ed2d074b5f9c Mon Sep 17 00:00:00 2001 From: "lightwalker.eth" <126201998+lightwalker-eth@users.noreply.github.com> Date: Tue, 26 Aug 2025 11:29:17 +0300 Subject: [PATCH 42/61] Refine docs on no-op case --- .../src/plugins/tokenscope/handlers/Seaport.ts | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/apps/ensindexer/src/plugins/tokenscope/handlers/Seaport.ts b/apps/ensindexer/src/plugins/tokenscope/handlers/Seaport.ts index 89292fb23..0ba812d66 100644 --- a/apps/ensindexer/src/plugins/tokenscope/handlers/Seaport.ts +++ b/apps/ensindexer/src/plugins/tokenscope/handlers/Seaport.ts @@ -18,6 +18,18 @@ export default function () { ponder.on(namespaceContract(pluginName, "Seaport:OrderFulfilled"), async ({ context, event }) => { const sale = getSupportedSaleFromOrderFulfilledEvent(config.namespace, context.chain.id, event); + // the Seaport sale in the event is not supported by TokenScope, no-op + // this can happen for a number of reasons, including: + // - the sale was for a NFT that is not recognized as being associated with an ENS name + // (note how this event is triggered for any sale made through Seaport, not just ENS names) + // - the sale was not paid for with a supported currency (e.g. ETH, USDC, etc) + // - the sale received payments in multiple currencies + // - the sale was for multiple NFTs (not just one) + // TokenScope purposefully does not support these cases as we believe they overall add + // more complexity than benefit. We believe it's better to prioritize better simplicity of + // building apps on TokenScope than supporting these more complex and uncommon cases. + if (!sale) return; + // TODO: remove these invariants and just store as bigint like god intended if (event.block.timestamp > BigInt(Number.MAX_SAFE_INTEGER)) { throw new Error( @@ -34,9 +46,6 @@ export default function () { const blockNumber = Number(event.block.number); const timestamp = Number(event.block.timestamp); - // no supported sale detected in event, no-op - if (!sale) return; - // upsert buyer and seller accounts await upsertAccount(context, sale.seller); await upsertAccount(context, sale.buyer); From 428b36dbaaefd172fa5ee41a6f8d23f2af87167a Mon Sep 17 00:00:00 2001 From: "lightwalker.eth" <126201998+lightwalker-eth@users.noreply.github.com> Date: Tue, 26 Aug 2025 11:35:14 +0300 Subject: [PATCH 43/61] Fix data model --- .../plugins/tokenscope/handlers/Seaport.ts | 20 ++----------------- .../ensnode-schema/src/tokenscope.schema.ts | 4 ++-- 2 files changed, 4 insertions(+), 20 deletions(-) diff --git a/apps/ensindexer/src/plugins/tokenscope/handlers/Seaport.ts b/apps/ensindexer/src/plugins/tokenscope/handlers/Seaport.ts index 0ba812d66..686876322 100644 --- a/apps/ensindexer/src/plugins/tokenscope/handlers/Seaport.ts +++ b/apps/ensindexer/src/plugins/tokenscope/handlers/Seaport.ts @@ -30,22 +30,6 @@ export default function () { // building apps on TokenScope than supporting these more complex and uncommon cases. if (!sale) return; - // TODO: remove these invariants and just store as bigint like god intended - if (event.block.timestamp > BigInt(Number.MAX_SAFE_INTEGER)) { - throw new Error( - `Error building onchain event ref: block timestamp is too large: ${event.block.timestamp}`, - ); - } - - if (event.block.number > BigInt(Number.MAX_SAFE_INTEGER)) { - throw new Error( - `Error building onchain event ref: block number is too large: ${event.block.number}`, - ); - } - - const blockNumber = Number(event.block.number); - const timestamp = Number(event.block.timestamp); - // upsert buyer and seller accounts await upsertAccount(context, sale.seller); await upsertAccount(context, sale.buyer); @@ -54,7 +38,7 @@ export default function () { await context.db.insert(schema.nameSales).values({ id: makeEventId(context.chain.id, event.block.number, event.log.logIndex), chainId: sale.nft.contract.chainId, - blockNumber, + blockNumber: event.block.number, logIndex: event.log.logIndex, transactionHash: event.transaction.hash, orderHash: sale.orderHash, @@ -74,7 +58,7 @@ export default function () { seller: sale.seller, currency: sale.payment.price.currency, amount: sale.payment.price.amount, - timestamp, + timestamp: event.block.timestamp, }); }); } diff --git a/packages/ensnode-schema/src/tokenscope.schema.ts b/packages/ensnode-schema/src/tokenscope.schema.ts index 3ac2974e9..c65c2a1b5 100644 --- a/packages/ensnode-schema/src/tokenscope.schema.ts +++ b/packages/ensnode-schema/src/tokenscope.schema.ts @@ -18,7 +18,7 @@ export const nameSales = onchainTable( /** * The block number on chainId where the sale occurred. */ - blockNumber: t.integer().notNull(), + blockNumber: t.bigint().notNull(), /** * The log index position of the sale event within blockNumber. @@ -104,7 +104,7 @@ export const nameSales = onchainTable( /** * Unix timestamp of the block timestamp when the sale occurred. */ - timestamp: t.integer().notNull(), + timestamp: t.bigint().notNull(), }), (t) => ({ idx_domainId: index().on(t.domainId), From fa9e48820301ad2515710ec41d2019bb3a5607ed Mon Sep 17 00:00:00 2001 From: "lightwalker.eth" <126201998+lightwalker-eth@users.noreply.github.com> Date: Tue, 26 Aug 2025 11:50:58 +0300 Subject: [PATCH 44/61] Add validation that supported NFTs are exactly 1 --- apps/ensindexer/src/lib/tokenscope/seaport.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/apps/ensindexer/src/lib/tokenscope/seaport.ts b/apps/ensindexer/src/lib/tokenscope/seaport.ts index f1e9429b4..55daa6de4 100644 --- a/apps/ensindexer/src/lib/tokenscope/seaport.ts +++ b/apps/ensindexer/src/lib/tokenscope/seaport.ts @@ -44,6 +44,9 @@ const getSupportedNFT = ( chainId: ChainId, item: SeaportOfferItem | SeaportConsiderationItem, ): SupportedNFT | null => { + // validate as exactly 1 item + if (item.amount !== 1n) return null; + // validate item as an ERC721/ERC1155 NFT const tokenType = getAssetNamespace(item.itemType); if (!tokenType) return null; From 83cb1ed662db3f0678a8923c5d6690f13a6162bb Mon Sep 17 00:00:00 2001 From: "lightwalker.eth" <126201998+lightwalker-eth@users.noreply.github.com> Date: Tue, 26 Aug 2025 11:52:38 +0300 Subject: [PATCH 45/61] Align language --- apps/ensindexer/src/lib/tokenscope/seaport.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/ensindexer/src/lib/tokenscope/seaport.ts b/apps/ensindexer/src/lib/tokenscope/seaport.ts index 55daa6de4..bdd825076 100644 --- a/apps/ensindexer/src/lib/tokenscope/seaport.ts +++ b/apps/ensindexer/src/lib/tokenscope/seaport.ts @@ -48,8 +48,8 @@ const getSupportedNFT = ( if (item.amount !== 1n) return null; // validate item as an ERC721/ERC1155 NFT - const tokenType = getAssetNamespace(item.itemType); - if (!tokenType) return null; + const assetNamespace = getAssetNamespace(item.itemType); + if (!assetNamespace) return null; // validate that the token is a known token issuing contract const tokenIssuer = getKnownTokenIssuer(namespaceId, { @@ -63,7 +63,7 @@ const getSupportedNFT = ( const domainId = tokenIssuer.getDomainId(tokenId); return { - assetNamespace: tokenType, + assetNamespace, contract, tokenId, domainId, From 0632022cd6790d71fea48e59d93b1741654f517c Mon Sep 17 00:00:00 2001 From: "lightwalker.eth" <126201998+lightwalker-eth@users.noreply.github.com> Date: Tue, 26 Aug 2025 11:54:20 +0300 Subject: [PATCH 46/61] Refine language --- apps/ensindexer/src/lib/tokenscope/seaport.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/ensindexer/src/lib/tokenscope/seaport.ts b/apps/ensindexer/src/lib/tokenscope/seaport.ts index bdd825076..5d343471e 100644 --- a/apps/ensindexer/src/lib/tokenscope/seaport.ts +++ b/apps/ensindexer/src/lib/tokenscope/seaport.ts @@ -14,9 +14,9 @@ import { ChainId, uniq } from "@ensnode/ensnode-sdk"; /** * Gets the supported TokenScope Asset Namespace for a given Seaport ItemType. * - * @param itemType - The Seaport item type to get the supported TokenScope token type for + * @param itemType - The Seaport item type to get the supported TokenScope asset namespace for * @returns the supported TokenScope Asset Namespace for the given Seaport ItemType, or null - * if the ItemType is not supported. + * if the Seaport item type is not supported. */ const getAssetNamespace = (itemType: SeaportItemType): AssetNamespace | null => { switch (itemType) { From 02e6fce693c689bb30a5036f28c87e0163c2eb71 Mon Sep 17 00:00:00 2001 From: "lightwalker.eth" <126201998+lightwalker-eth@users.noreply.github.com> Date: Tue, 26 Aug 2025 11:57:44 +0300 Subject: [PATCH 47/61] Refactor currency --- apps/ensindexer/src/lib/tokenscope/seaport.ts | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/apps/ensindexer/src/lib/tokenscope/seaport.ts b/apps/ensindexer/src/lib/tokenscope/seaport.ts index 5d343471e..4af8e7947 100644 --- a/apps/ensindexer/src/lib/tokenscope/seaport.ts +++ b/apps/ensindexer/src/lib/tokenscope/seaport.ts @@ -76,21 +76,21 @@ const getSupportedPayment = ( item: SeaportOfferItem | SeaportConsiderationItem, ): SupportedPayment | null => { // validate that the item is a supported currency - const currencyId = getCurrencyIdForContract(namespaceId, { + const currency = getCurrencyIdForContract(namespaceId, { chainId, address: item.token, }); - if (!currencyId) return null; // Unsupported currency + if (!currency) return null; // Unsupported currency - // validate the Seaport item type is supported and matches the currencyId + // validate the Seaport item type is supported and matches the currency if (item.itemType === SeaportItemType.NATIVE) { - if (currencyId !== CurrencyIds.ETH) { - return null; // Seaport item type doesn't match currencyId + if (currency !== CurrencyIds.ETH) { + return null; // Seaport item type doesn't match currency } } else if (item.itemType === SeaportItemType.ERC20) { - if (currencyId === CurrencyIds.ETH) { - return null; // Seaport item type doesn't match currencyId + if (currency === CurrencyIds.ETH) { + return null; // Seaport item type doesn't match currency } } else { // unsupported Seaport item type @@ -101,7 +101,7 @@ const getSupportedPayment = ( return { price: { - currency: currencyId, + currency, amount: item.amount, }, }; From 0d150b1f6c7b21b44499b96ef34dd13d629cf2a5 Mon Sep 17 00:00:00 2001 From: "lightwalker.eth" <126201998+lightwalker-eth@users.noreply.github.com> Date: Tue, 26 Aug 2025 11:59:02 +0300 Subject: [PATCH 48/61] Refine docs --- apps/ensindexer/src/lib/tokenscope/seaport.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/ensindexer/src/lib/tokenscope/seaport.ts b/apps/ensindexer/src/lib/tokenscope/seaport.ts index 4af8e7947..2b29ebeeb 100644 --- a/apps/ensindexer/src/lib/tokenscope/seaport.ts +++ b/apps/ensindexer/src/lib/tokenscope/seaport.ts @@ -115,8 +115,8 @@ interface SeaportItemExtractions { * * Example cases include: * - Payments are being made in multiple currencies. - * - Multiple payments in the same currency, but where payment is for marketplace fees while - * other payments are for the seller. + * - Multiple payments in the same currency, but where one payment is for marketplace fees while + * another payment is for the seller. */ payments: SupportedPayment[]; } From 4cf08d31c9f5a61db525f6b63d6098718c5cbfc1 Mon Sep 17 00:00:00 2001 From: "lightwalker.eth" <126201998+lightwalker-eth@users.noreply.github.com> Date: Tue, 26 Aug 2025 12:08:41 +0300 Subject: [PATCH 49/61] refine docs --- apps/ensindexer/src/lib/datasource-helpers.ts | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/apps/ensindexer/src/lib/datasource-helpers.ts b/apps/ensindexer/src/lib/datasource-helpers.ts index e0489d096..a3136f765 100644 --- a/apps/ensindexer/src/lib/datasource-helpers.ts +++ b/apps/ensindexer/src/lib/datasource-helpers.ts @@ -22,18 +22,18 @@ export const getChainIdsInNamespace = (namespaceId: ENSNamespaceId): ChainId[] = }; /** - * Gets the AccountId of the requested contract for the specified namespace, datasource, and + * Gets the AccountId for the contract in the specified namespace, datasource, and * contract name, or undefined if it is not defined or is not a single AccountId. * - * This is useful when you want to retrieve a single AccountId for an arbitrary contract - * where it may or may not actually be defined. + * This is useful when you want to retrieve the AccountId for a contract by its name + * where it may or may not actually be defined for the given namespace and datasource. * * @param namespaceId - The ENSNamespace identifier (e.g. 'mainnet', 'sepolia', 'holesky', * 'ens-test-env') * @param datasourceName - The name of the Datasource to search for contractName in - * @param contractName - The name of the contract to retrieve the AccountId for - * @returns The AccountId of the requested contract for the given namespace, datasource, - * and contract name, or undefined if it does not exist or is not a single AccountId + * @param contractName - The name of the contract to retrieve + * @returns The AccountId of the contract with the given namespace, datasource, + * and contract name, or undefined if it is not found or is not a single AccountId */ export const maybeGetDatasourceContract = ( namespaceId: ENSNamespaceId, @@ -43,11 +43,11 @@ export const maybeGetDatasourceContract = ( const datasource = maybeGetDatasource(namespaceId, datasourceName); if (!datasource) return undefined; - const maybeAddress = datasource.contracts[contractName]?.address; - if (maybeAddress === undefined || Array.isArray(maybeAddress)) return undefined; + const address = datasource.contracts[contractName]?.address; + if (address === undefined || Array.isArray(address)) return undefined; return { chainId: datasource.chain.id, - address: maybeAddress, + address, }; }; From 461c807afdbe9fb8c1af153b15fb06d31aee743d Mon Sep 17 00:00:00 2001 From: "lightwalker.eth" <126201998+lightwalker-eth@users.noreply.github.com> Date: Tue, 26 Aug 2025 12:18:39 +0300 Subject: [PATCH 50/61] Constrain exports --- apps/ensindexer/src/lib/currencies.ts | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/apps/ensindexer/src/lib/currencies.ts b/apps/ensindexer/src/lib/currencies.ts index d0eed5c52..a0d6896c9 100644 --- a/apps/ensindexer/src/lib/currencies.ts +++ b/apps/ensindexer/src/lib/currencies.ts @@ -108,6 +108,8 @@ const knownCurrencyContracts: Record> = { [CurrencyIds.USDC]: "0x176211869cA2b568f2A7D4EE941E073a821EE1ff", [CurrencyIds.DAI]: "0x4AF15ec2A0BD43Db75dd04E62FAA3B8EF36b00d5", }, + + /** holesky namespace */ [holesky.id]: { [CurrencyIds.ETH]: zeroAddress, [CurrencyIds.USDC]: "0x1c7D4B196Cb0C7B01d743Fbc6116a902379C7238", @@ -121,7 +123,7 @@ const knownCurrencyContracts: Record> = { * @param chainId - The chain ID to get supported currency contracts for * @returns a record of currency ids to AccountIds for the given chain */ -export const getSupportedCurrencyContractsForChain = ( +const getSupportedCurrencyContractsForChain = ( chainId: ChainId, ): Record => { let result = {} as Record; @@ -146,7 +148,7 @@ export const getSupportedCurrencyContractsForChain = ( * 'ens-test-env') * @returns a record of currency ids to AccountIds for the given namespace */ -export const getSupportedCurrencyContractsForNamespace = ( +const getSupportedCurrencyContractsForNamespace = ( namespaceId: ENSNamespaceId, ): Record => { let result = {} as Record; @@ -168,7 +170,7 @@ export const getSupportedCurrencyContractsForNamespace = ( * @returns a boolean indicating if the provided AccountId is a supported currency * contract in the specified namespace */ -export const isSupportedCurrencyContract = ( +const isSupportedCurrencyContract = ( namespaceId: ENSNamespaceId, contract: AccountId, ): boolean => { From d15e9af0661d4d67089780ecdf6a5b8b652920fe Mon Sep 17 00:00:00 2001 From: "lightwalker.eth" <126201998+lightwalker-eth@users.noreply.github.com> Date: Tue, 26 Aug 2025 12:19:15 +0300 Subject: [PATCH 51/61] Remove unused function --- apps/ensindexer/src/lib/currencies.ts | 19 ------------------- 1 file changed, 19 deletions(-) diff --git a/apps/ensindexer/src/lib/currencies.ts b/apps/ensindexer/src/lib/currencies.ts index a0d6896c9..9ed90d648 100644 --- a/apps/ensindexer/src/lib/currencies.ts +++ b/apps/ensindexer/src/lib/currencies.ts @@ -161,25 +161,6 @@ const getSupportedCurrencyContractsForNamespace = ( return result; }; -/** - * Identifies if the provided AccountId is a supported currency contract in the specified namespace. - * - * @param namespaceId - The ENSNamespace identifier (e.g. 'mainnet', 'sepolia', 'holesky', - * 'ens-test-env') - * @param contract - The AccountId of the contract to check - * @returns a boolean indicating if the provided AccountId is a supported currency - * contract in the specified namespace - */ -const isSupportedCurrencyContract = ( - namespaceId: ENSNamespaceId, - contract: AccountId, -): boolean => { - const supportedCurrencyContracts = getSupportedCurrencyContractsForNamespace(namespaceId); - return Object.values(supportedCurrencyContracts).some((supportedCurrencyContract) => - accountIdEqual(supportedCurrencyContract, contract), - ); -}; - /** * Gets the currency id for the given contract in the specified namespace. * From d02e9cf9a63c2984d787d53cf34778fb991cad5c Mon Sep 17 00:00:00 2001 From: "lightwalker.eth" <126201998+lightwalker-eth@users.noreply.github.com> Date: Tue, 26 Aug 2025 12:20:47 +0300 Subject: [PATCH 52/61] lint --- apps/ensindexer/src/lib/currencies.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/apps/ensindexer/src/lib/currencies.ts b/apps/ensindexer/src/lib/currencies.ts index 9ed90d648..ba0c2348d 100644 --- a/apps/ensindexer/src/lib/currencies.ts +++ b/apps/ensindexer/src/lib/currencies.ts @@ -123,9 +123,7 @@ const knownCurrencyContracts: Record> = { * @param chainId - The chain ID to get supported currency contracts for * @returns a record of currency ids to AccountIds for the given chain */ -const getSupportedCurrencyContractsForChain = ( - chainId: ChainId, -): Record => { +const getSupportedCurrencyContractsForChain = (chainId: ChainId): Record => { let result = {} as Record; const knownCurrencyContractsForChain = knownCurrencyContracts[chainId]; From 73d04db80eaca47a5b68ea1131516a105dac0c80 Mon Sep 17 00:00:00 2001 From: "lightwalker.eth" <126201998+lightwalker-eth@users.noreply.github.com> Date: Tue, 26 Aug 2025 12:25:58 +0300 Subject: [PATCH 53/61] constrain exports --- apps/ensindexer/src/lib/tokenscope/token-issuers.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/ensindexer/src/lib/tokenscope/token-issuers.ts b/apps/ensindexer/src/lib/tokenscope/token-issuers.ts index d5550eb91..acb4e4a32 100644 --- a/apps/ensindexer/src/lib/tokenscope/token-issuers.ts +++ b/apps/ensindexer/src/lib/tokenscope/token-issuers.ts @@ -37,7 +37,7 @@ export interface TokenIssuer { * @param tokenId - The tokenId to convert * @returns The Node of the tokenId */ -export const nameHashGeneratedTokenIdToNode = (tokenId: TokenId): Node => { +const nameHashGeneratedTokenIdToNode = (tokenId: TokenId): Node => { return uint256ToHex32(tokenId); }; @@ -50,7 +50,7 @@ export const nameHashGeneratedTokenIdToNode = (tokenId: TokenId): Node => { * @param parentNode - the parent Node that the token issuing contract issues subnames under * @returns The Node of the tokenId issued under the parentNode */ -export const labelHashGeneratedTokenIdToNode = (tokenId: TokenId, parentNode: Node): Node => { +const labelHashGeneratedTokenIdToNode = (tokenId: TokenId, parentNode: Node): Node => { const labelHash: LabelHash = uint256ToHex32(tokenId); return makeSubdomainNode(labelHash, parentNode); }; @@ -62,7 +62,7 @@ export const labelHashGeneratedTokenIdToNode = (tokenId: TokenId, parentNode: No * @param namespaceId - The ENSNamespace identifier (e.g. 'mainnet', 'sepolia', 'holesky', 'ens-test-env') * @returns an array of 0 or more known TokenIssuer for the specified namespace */ -export const getKnownTokenIssuers = (namespaceId: ENSNamespaceId): TokenIssuer[] => { +const getKnownTokenIssuers = (namespaceId: ENSNamespaceId): TokenIssuer[] => { const ethBaseRegistrar = maybeGetDatasourceContract( namespaceId, DatasourceNames.ENSRoot, From 4f6d02902e08ffd9e91195f9ac0dbe4b78338cc4 Mon Sep 17 00:00:00 2001 From: "lightwalker.eth" <126201998+lightwalker-eth@users.noreply.github.com> Date: Tue, 26 Aug 2025 12:38:01 +0300 Subject: [PATCH 54/61] Refine docs --- packages/ensnode-schema/src/tokenscope.schema.ts | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/packages/ensnode-schema/src/tokenscope.schema.ts b/packages/ensnode-schema/src/tokenscope.schema.ts index c65c2a1b5..64f7de1f1 100644 --- a/packages/ensnode-schema/src/tokenscope.schema.ts +++ b/packages/ensnode-schema/src/tokenscope.schema.ts @@ -31,7 +31,7 @@ export const nameSales = onchainTable( transactionHash: t.hex().notNull(), /** - * The order hash of the sale order within contractAddress. + * The Seaport order hash. */ orderHash: t.hex().notNull(), @@ -43,12 +43,9 @@ export const nameSales = onchainTable( /** * The tokenId managed by contractAddress that was sold. * - * Combined with `chainId` and `contractAddress`, creates unique NFT identifier - * for the domain being transferred. - * - * Interpretation depends on 'tokenType': - * - ERC721: Unique token within contract - * - ERC1155: Token type identifier (multiple copies may exist) + * Interpretation depends on 'assetNamespace': + * - erc721: Unique token within contract + * - erc1155: Token type identifier (multiple copies may exist) */ tokenId: t.bigint().notNull(), From 7c26c8dc8463d5bf7a0edc6380010a6656505475 Mon Sep 17 00:00:00 2001 From: "lightwalker.eth" <126201998+lightwalker-eth@users.noreply.github.com> Date: Tue, 26 Aug 2025 15:49:36 +0300 Subject: [PATCH 55/61] Refine docs on hosted instances --- .../src/content/docs/docs/usage/hosted-ensnode-instances.mdx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/ensnode.io/src/content/docs/docs/usage/hosted-ensnode-instances.mdx b/docs/ensnode.io/src/content/docs/docs/usage/hosted-ensnode-instances.mdx index 0d752fcbc..c1418fbf2 100644 --- a/docs/ensnode.io/src/content/docs/docs/usage/hosted-ensnode-instances.mdx +++ b/docs/ensnode.io/src/content/docs/docs/usage/hosted-ensnode-instances.mdx @@ -31,7 +31,7 @@ These ENSNode instances focus on pushing the boundaries of what's possible with connectWithENSAdminURL="https://admin.ensnode.io/connect?ensnode=https%3A%2F%2Fapi.alpha.ensnode.io" namespace="mainnet" plugins="subgraph, basenames, lineanames, threedns, reverse-resolvers, referrals, tokenscope" - purpose="Demonstration of ENSNode's multichain ENS indexing capability for the ENS namespace rooted on Mainnet. Provides a superset of the data indexed by the ENS Subgraph. Indexes additional Resolver record values in preparation for ENS Protocol Acceleration." + purpose="Demonstration of ENSNode's multichain ENS indexing capability for the ENS namespace rooted on Mainnet. Provides a superset of the data indexed by the ENS Subgraph. Offers a preview of ENS Protocol Acceleration." /> #### ENSNode 'Alpha-Sepolia' @@ -40,7 +40,7 @@ These ENSNode instances focus on pushing the boundaries of what's possible with connectWithENSAdminURL="https://admin.ensnode.io/connect?ensnode=https%3A%2F%2Fapi.alpha-sepolia.ensnode.io" namespace="sepolia" plugins="subgraph, basenames, lineanames, reverse-resolvers, referrals, tokenscope" - purpose="Demonstration of ENSNode's multichain ENS indexing capability for the ENS namespace rooted on Sepolia. Provides a superset of the data indexed by the ENS Subgraph. Indexes additional Resolver record values in preparation for ENS Protocol Acceleration." + purpose="Demonstration of ENSNode's multichain ENS indexing capability for the ENS namespace rooted on Sepolia. Provides a superset of the data indexed by the ENS Subgraph. Offers a preview of ENS Protocol Acceleration." /> ### Subgraph-style Deployments From 48689a45c32a2b4a2e8bea06fdda7e7774b5f8fe Mon Sep 17 00:00:00 2001 From: shrugs Date: Tue, 26 Aug 2025 11:57:07 -0500 Subject: [PATCH 56/61] fix: remove Seaport* prefix for seaport-types --- .../src/lib/tokenscope/seaport-types.ts | 16 +++++----- apps/ensindexer/src/lib/tokenscope/seaport.ts | 29 +++++++++---------- .../plugins/tokenscope/handlers/Seaport.ts | 7 ++--- 3 files changed, 24 insertions(+), 28 deletions(-) diff --git a/apps/ensindexer/src/lib/tokenscope/seaport-types.ts b/apps/ensindexer/src/lib/tokenscope/seaport-types.ts index f3ce04872..8cb05b9cb 100644 --- a/apps/ensindexer/src/lib/tokenscope/seaport-types.ts +++ b/apps/ensindexer/src/lib/tokenscope/seaport-types.ts @@ -9,7 +9,7 @@ import { Address, Hex } from "viem"; * * @see https://github.com/ProjectOpenSea/seaport-js/blob/c4d4756c8000a7143fc1ed9a5aad71b444ae90b4/src/constants.ts#L89 */ -export enum SeaportItemType { +export enum ItemType { NATIVE = 0, ERC20 = 1, ERC721 = 2, @@ -21,7 +21,7 @@ export enum SeaportItemType { /** * A Seaport OrderFulfilled Event from Ponder, semantically typed with descriptions. */ -export type SeaportOrderFulfilledEvent = EventWithArgs<{ +export type OrderFulfilledEvent = EventWithArgs<{ /** * The unique hash identifier of the fulfilled order within Seaport. * Used to track and reference specific orders on-chain. @@ -53,22 +53,22 @@ export type SeaportOrderFulfilledEvent = EventWithArgs<{ * For listings: NFTs/tokens being sold * For offers: ETH/ERC20 tokens being offered as payment */ - offer: readonly SeaportOfferItem[]; + offer: readonly OfferItem[]; /** * Array of items that the offerer expects to receive in return. * For listings: ETH/ERC20 tokens expected as payment * For offers: NFTs/tokens being requested in exchange */ - consideration: readonly SeaportConsiderationItem[]; + consideration: readonly ConsiderationItem[]; }>; -export type SeaportOfferItem = { +export type OfferItem = { /** * The type of item in the offer. * For example, ERC20, ERC721, ERC1155, or NATIVE (ETH) */ - itemType: SeaportItemType; + itemType: ItemType; /** * The contract address of the token. @@ -96,12 +96,12 @@ export type SeaportOfferItem = { amount: bigint; }; -export type SeaportConsiderationItem = { +export type ConsiderationItem = { /** * The type of item in the consideration. * For example, ERC20, ERC721, ERC1155, or NATIVE (ETH) */ - itemType: SeaportItemType; + itemType: ItemType; /** * The contract address of the token. diff --git a/apps/ensindexer/src/lib/tokenscope/seaport.ts b/apps/ensindexer/src/lib/tokenscope/seaport.ts index 2b29ebeeb..6715438c0 100644 --- a/apps/ensindexer/src/lib/tokenscope/seaport.ts +++ b/apps/ensindexer/src/lib/tokenscope/seaport.ts @@ -2,10 +2,10 @@ import { CurrencyIds, getCurrencyIdForContract } from "@/lib/currencies"; import { AssetNamespace, AssetNamespaces } from "@/lib/tokenscope/assets"; import { SupportedNFT, SupportedPayment, SupportedSale } from "@/lib/tokenscope/sales"; import { - SeaportConsiderationItem, - SeaportItemType, - SeaportOfferItem, - SeaportOrderFulfilledEvent, + ConsiderationItem, + ItemType, + OfferItem, + OrderFulfilledEvent, } from "@/lib/tokenscope/seaport-types"; import { getKnownTokenIssuer } from "@/lib/tokenscope/token-issuers"; import { ENSNamespaceId } from "@ensnode/datasources"; @@ -18,11 +18,11 @@ import { ChainId, uniq } from "@ensnode/ensnode-sdk"; * @returns the supported TokenScope Asset Namespace for the given Seaport ItemType, or null * if the Seaport item type is not supported. */ -const getAssetNamespace = (itemType: SeaportItemType): AssetNamespace | null => { +const getAssetNamespace = (itemType: ItemType): AssetNamespace | null => { switch (itemType) { - case SeaportItemType.ERC721: + case ItemType.ERC721: return AssetNamespaces.ERC721; - case SeaportItemType.ERC1155: + case ItemType.ERC1155: return AssetNamespaces.ERC1155; default: return null; @@ -42,7 +42,7 @@ const getAssetNamespace = (itemType: SeaportItemType): AssetNamespace | null => const getSupportedNFT = ( namespaceId: ENSNamespaceId, chainId: ChainId, - item: SeaportOfferItem | SeaportConsiderationItem, + item: OfferItem | ConsiderationItem, ): SupportedNFT | null => { // validate as exactly 1 item if (item.amount !== 1n) return null; @@ -73,7 +73,7 @@ const getSupportedNFT = ( const getSupportedPayment = ( namespaceId: ENSNamespaceId, chainId: ChainId, - item: SeaportOfferItem | SeaportConsiderationItem, + item: OfferItem | ConsiderationItem, ): SupportedPayment | null => { // validate that the item is a supported currency const currency = getCurrencyIdForContract(namespaceId, { @@ -84,11 +84,11 @@ const getSupportedPayment = ( if (!currency) return null; // Unsupported currency // validate the Seaport item type is supported and matches the currency - if (item.itemType === SeaportItemType.NATIVE) { + if (item.itemType === ItemType.NATIVE) { if (currency !== CurrencyIds.ETH) { return null; // Seaport item type doesn't match currency } - } else if (item.itemType === SeaportItemType.ERC20) { + } else if (item.itemType === ItemType.ERC20) { if (currency === CurrencyIds.ETH) { return null; // Seaport item type doesn't match currency } @@ -124,7 +124,7 @@ interface SeaportItemExtractions { const getSeaportItemExtractions = ( namespaceId: ENSNamespaceId, chainId: ChainId, - items: readonly (SeaportOfferItem | SeaportConsiderationItem)[], + items: readonly (OfferItem | ConsiderationItem)[], ): SeaportItemExtractions => { const extractions: SeaportItemExtractions = { nfts: [], @@ -155,8 +155,7 @@ const consolidateSupportedNFTs = (nfts: SupportedNFT[]): SupportedNFT | null => const consolidateSupportedPayments = (payments: SupportedPayment[]): SupportedPayment | null => { // Get the set of distinct currencies in the payment - const paymentCurrencies = payments.map((payment) => payment.price.currency); - const uniqueCurrencies = uniq(paymentCurrencies); + const uniqueCurrencies = uniq(payments.map((payment) => payment.price.currency)); // Either no payment or multiple payments in mixed currencies if (uniqueCurrencies.length !== 1) return null; @@ -180,7 +179,7 @@ const consolidateSupportedPayments = (payments: SupportedPayment[]): SupportedPa export const getSupportedSaleFromOrderFulfilledEvent = ( namespaceId: ENSNamespaceId, chainId: ChainId, - event: SeaportOrderFulfilledEvent, + event: OrderFulfilledEvent, ): SupportedSale | null => { const { offer, consideration, orderHash, offerer, recipient } = event.args; diff --git a/apps/ensindexer/src/plugins/tokenscope/handlers/Seaport.ts b/apps/ensindexer/src/plugins/tokenscope/handlers/Seaport.ts index 686876322..45faaa886 100644 --- a/apps/ensindexer/src/plugins/tokenscope/handlers/Seaport.ts +++ b/apps/ensindexer/src/plugins/tokenscope/handlers/Seaport.ts @@ -1,7 +1,7 @@ import { ponder } from "ponder:registry"; import schema from "ponder:schema"; import { PluginName, uint256ToHex32 } from "@ensnode/ensnode-sdk"; -import { AssetId, ChainId } from "caip"; +import { AssetId } from "caip"; import config from "@/config"; import { upsertAccount } from "@/lib/db-helpers"; @@ -46,10 +46,7 @@ export default function () { tokenId: sale.nft.tokenId, assetNamespace: sale.nft.assetNamespace, assetId: AssetId.format({ - chainId: ChainId.format({ - namespace: "eip155", - reference: sale.nft.contract.chainId.toString(), - }), + chainId: { namespace: "eip155", reference: sale.nft.contract.chainId.toString() }, assetName: { namespace: sale.nft.assetNamespace, reference: sale.nft.contract.address }, tokenId: uint256ToHex32(sale.nft.tokenId), }), From 8eed36bccf6d619bb27ed0bbdd9bda53e81b65b8 Mon Sep 17 00:00:00 2001 From: shrugs Date: Tue, 26 Aug 2025 12:02:15 -0500 Subject: [PATCH 57/61] fix: remove unused function --- .../src/lib/tokenscope/token-issuers.ts | 29 ------------------- 1 file changed, 29 deletions(-) diff --git a/apps/ensindexer/src/lib/tokenscope/token-issuers.ts b/apps/ensindexer/src/lib/tokenscope/token-issuers.ts index acb4e4a32..5365e8dc6 100644 --- a/apps/ensindexer/src/lib/tokenscope/token-issuers.ts +++ b/apps/ensindexer/src/lib/tokenscope/token-issuers.ts @@ -169,32 +169,3 @@ export const getKnownTokenIssuer = ( const tokenIssuers = getKnownTokenIssuers(namespaceId); return tokenIssuers.find((tokenIssuer) => accountIdEqual(tokenIssuer.contract, contract)) ?? null; }; - -/** - * Gets the domainId (Node) for a given NFT from contract with tokenId on the specified namespace. - * - * @param namespaceId - The ENSNamespace identifier (e.g. 'mainnet', 'sepolia', 'holesky', - * 'ens-test-env') - * @param contract - The AccountId of the NFT contract - * @param tokenId - The tokenId of the NFT - * @returns the domainId (Node) for ENS name associated with the NFT - * @throws an error if the contract is not a known token issuing contract in the specified namespace - */ -export function getDomainIdByTokenId( - namespaceId: ENSNamespaceId, - contract: AccountId, - tokenId: TokenId, -): Node { - const tokenIssuers = getKnownTokenIssuers(namespaceId); - const tokenIssuer = tokenIssuers.find((tokenIssuer) => - accountIdEqual(tokenIssuer.contract, contract), - ); - - if (!tokenIssuer) { - throw new Error( - `The contract at address ${contract.address} on chain ${contract.chainId} is not a known token issuer in the ${namespaceId} namespace`, - ); - } - - return tokenIssuer.getDomainId(tokenId); -} From 4329fda6f65b6ae739144ec38dfbd8de9062fc74 Mon Sep 17 00:00:00 2001 From: shrugs Date: Tue, 26 Aug 2025 12:38:18 -0500 Subject: [PATCH 58/61] fix: clean up currency handling --- apps/ensindexer/src/lib/currencies.ts | 80 +++++-------------- apps/ensindexer/src/lib/datasource-helpers.ts | 15 ---- apps/ensindexer/src/lib/tokenscope/seaport.ts | 48 +++++------ apps/ensindexer/test/token-issuers.test.ts | 19 +++++ 4 files changed, 63 insertions(+), 99 deletions(-) create mode 100644 apps/ensindexer/test/token-issuers.test.ts diff --git a/apps/ensindexer/src/lib/currencies.ts b/apps/ensindexer/src/lib/currencies.ts index ba0c2348d..93da4917f 100644 --- a/apps/ensindexer/src/lib/currencies.ts +++ b/apps/ensindexer/src/lib/currencies.ts @@ -9,9 +9,7 @@ import { sepolia, } from "viem/chains"; -import { getChainIdsInNamespace } from "@/lib/datasource-helpers"; -import { ENSNamespaceId, getENSNamespace } from "@ensnode/datasources"; -import { AccountId, ChainId, accountIdEqual, uniq } from "@ensnode/ensnode-sdk"; +import { AccountId, ChainId, accountIdEqual } from "@ensnode/ensnode-sdk"; import { Address, zeroAddress } from "viem"; /** @@ -39,13 +37,13 @@ export interface Price { amount: bigint; } -export interface CurrencyConfig { +export interface CurrencyInfo { id: CurrencyId; name: string; decimals: number; } -const currencyConfigs: Record = { +const currencyInfo: Record = { [CurrencyIds.ETH]: { id: CurrencyIds.ETH, name: "Ethereum", @@ -63,13 +61,11 @@ const currencyConfigs: Record = { }, }; -export const getCurrencyConfig = (currencyId: CurrencyId): CurrencyConfig => { - return currencyConfigs[currencyId]; -}; +export const getCurrencyInfo = (currencyId: CurrencyId): CurrencyInfo => currencyInfo[currencyId]; // NOTE: this mapping currently only considers the subset of chains where we have // supported token issuing contracts. -const knownCurrencyContracts: Record> = { +const KNOWN_CURRENCY_CONTRACTS: Record> = { /** mainnet namespace */ [mainnet.id]: { [CurrencyIds.ETH]: zeroAddress, @@ -118,45 +114,18 @@ const knownCurrencyContracts: Record> = { } as const; /** - * Gets the supported currency contracts for a given chain. + * Gets the supported currency contracts for a given chain as a Record * * @param chainId - The chain ID to get supported currency contracts for * @returns a record of currency ids to AccountIds for the given chain */ const getSupportedCurrencyContractsForChain = (chainId: ChainId): Record => { - let result = {} as Record; - - const knownCurrencyContractsForChain = knownCurrencyContracts[chainId]; - if (!knownCurrencyContractsForChain) return result; - - for (const [currencyId, address] of Object.entries(knownCurrencyContractsForChain)) { - result[currencyId as CurrencyId] = { - address, - chainId, - } as AccountId; - } - - return result; -}; - -/** - * Gets the supported currency contracts for a given namespace. - * - * @param namespaceId - The ENSNamespace identifier (e.g. 'mainnet', 'sepolia', 'holesky', - * 'ens-test-env') - * @returns a record of currency ids to AccountIds for the given namespace - */ -const getSupportedCurrencyContractsForNamespace = ( - namespaceId: ENSNamespaceId, -): Record => { - let result = {} as Record; - const chainIds = getChainIdsInNamespace(namespaceId); - for (const chainId of chainIds) { - const supportedCurrencyContractsForChain = getSupportedCurrencyContractsForChain(chainId); - result = { ...result, ...supportedCurrencyContractsForChain }; - } - - return result; + return Object.fromEntries( + Object.entries(KNOWN_CURRENCY_CONTRACTS[chainId] ?? {}).map(([currencyId, address]) => [ + currencyId, + { chainId, address }, + ]), + ) as Record; }; /** @@ -169,19 +138,14 @@ const getSupportedCurrencyContractsForNamespace = ( * null if the contract is not a supported currency contract in the * specified namespace */ -export const getCurrencyIdForContract = ( - namespaceId: ENSNamespaceId, - contract: AccountId, -): CurrencyId | null => { - const supportedCurrencyContracts = getSupportedCurrencyContractsForNamespace(namespaceId); - - for (const [currencyId, supportedCurrencyContract] of Object.entries( - supportedCurrencyContracts, - )) { - if (accountIdEqual(supportedCurrencyContract, contract)) { - return currencyId as CurrencyId; - } - } - - return null; +export const getCurrencyIdForContract = (contract: AccountId): CurrencyId | null => { + const supportedCurrencyContracts = getSupportedCurrencyContractsForChain(contract.chainId); + + const found = Object.entries(supportedCurrencyContracts).find(([, accountId]) => + accountIdEqual(accountId, contract), + ); + + if (!found) return null; + + return found[0] as CurrencyId; }; diff --git a/apps/ensindexer/src/lib/datasource-helpers.ts b/apps/ensindexer/src/lib/datasource-helpers.ts index a3136f765..0fef5cfaf 100644 --- a/apps/ensindexer/src/lib/datasource-helpers.ts +++ b/apps/ensindexer/src/lib/datasource-helpers.ts @@ -6,21 +6,6 @@ import { } from "@ensnode/datasources"; import { AccountId, ChainId, uniq } from "@ensnode/ensnode-sdk"; -/** - * Gets all the distinct chainIds used in the specified namespace. - * - * NOTE: This takes no consideration of which datasources are configured for indexing. - * - * @param namespaceId - The ENSNamespace identifier (e.g. 'mainnet', 'sepolia', 'holesky', - * 'ens-test-env') - * @returns an array of distinct chainIds with datasources in the specified namespace - */ -export const getChainIdsInNamespace = (namespaceId: ENSNamespaceId): ChainId[] => { - const namespace = getENSNamespace(namespaceId); - const chainIds = Object.values(namespace).map((datasource) => datasource.chain.id); - return uniq(chainIds); -}; - /** * Gets the AccountId for the contract in the specified namespace, datasource, and * contract name, or undefined if it is not defined or is not a single AccountId. diff --git a/apps/ensindexer/src/lib/tokenscope/seaport.ts b/apps/ensindexer/src/lib/tokenscope/seaport.ts index 6715438c0..3ed06fa2f 100644 --- a/apps/ensindexer/src/lib/tokenscope/seaport.ts +++ b/apps/ensindexer/src/lib/tokenscope/seaport.ts @@ -71,37 +71,29 @@ const getSupportedNFT = ( }; const getSupportedPayment = ( - namespaceId: ENSNamespaceId, chainId: ChainId, item: OfferItem | ConsiderationItem, ): SupportedPayment | null => { + // only NATIVE and ERC20 Payments are supported at the moment + if (item.itemType !== ItemType.NATIVE && item.itemType !== ItemType.ERC20) return null; + // validate that the item is a supported currency - const currency = getCurrencyIdForContract(namespaceId, { - chainId, - address: item.token, - }); + const currencyId = getCurrencyIdForContract({ chainId, address: item.token }); + if (!currencyId) return null; - if (!currency) return null; // Unsupported currency + // Sanity Check: if ItemType.NATIVE, the inferred Currency must be ETH + if (item.itemType === ItemType.NATIVE && currencyId !== CurrencyIds.ETH) return null; - // validate the Seaport item type is supported and matches the currency - if (item.itemType === ItemType.NATIVE) { - if (currency !== CurrencyIds.ETH) { - return null; // Seaport item type doesn't match currency - } - } else if (item.itemType === ItemType.ERC20) { - if (currency === CurrencyIds.ETH) { - return null; // Seaport item type doesn't match currency - } - } else { - // unsupported Seaport item type - return null; - } + // Sanity Check: if ItemType.ERC20, the inferred Currency must NOT be ETH + if (item.itemType === ItemType.ERC20 && currencyId === CurrencyIds.ETH) return null; - if (item.amount < 0n) return null; // Invalid amount + // Sanity Check: amount of currency must be >=0 + if (item.amount < 0n) return null; + // finally, a valid, SupportedPayment return { price: { - currency, + currency: currencyId, amount: item.amount, }, }; @@ -133,14 +125,18 @@ const getSeaportItemExtractions = ( // each item is either a supported NFT, a supported payment, or unsupported for (const item of items) { + // if the item is an nft, push to nfts const nft = getSupportedNFT(namespaceId, chainId, item); if (nft) { extractions.nfts.push(nft); - } else { - const payment = getSupportedPayment(namespaceId, chainId, item); - if (payment) { - extractions.payments.push(payment); - } + continue; + } + + // if the item is a payment, push to payments + const payment = getSupportedPayment(chainId, item); + if (payment) { + extractions.payments.push(payment); + continue; } } diff --git a/apps/ensindexer/test/token-issuers.test.ts b/apps/ensindexer/test/token-issuers.test.ts new file mode 100644 index 000000000..358f98319 --- /dev/null +++ b/apps/ensindexer/test/token-issuers.test.ts @@ -0,0 +1,19 @@ +import { describe, expect, it } from "vitest"; + +import { DatasourceNames, ENSNamespaceIds, getDatasource } from "@ensnode/datasources"; +import { getKnownTokenIssuer } from "../src/lib/tokenscope/token-issuers"; + +const { + contracts: { NameWrapper }, +} = getDatasource(ENSNamespaceIds.Mainnet, DatasourceNames.ENSRoot); + +describe("token-issuers", () => { + it("works for expected mainnet contract", () => { + const issuer = getKnownTokenIssuer(ENSNamespaceIds.Mainnet, { + chainId: 1, + address: NameWrapper.address, + }); + + expect(issuer).toBeDefined(); + }); +}); From abfc68b43b2cce75be83119e5794e98c7912e9c5 Mon Sep 17 00:00:00 2001 From: shrugs Date: Tue, 26 Aug 2025 12:50:23 -0500 Subject: [PATCH 59/61] fix: boolean error in offer/consideration validity check --- apps/ensindexer/src/lib/tokenscope/seaport.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/apps/ensindexer/src/lib/tokenscope/seaport.ts b/apps/ensindexer/src/lib/tokenscope/seaport.ts index 3ed06fa2f..ffe55895e 100644 --- a/apps/ensindexer/src/lib/tokenscope/seaport.ts +++ b/apps/ensindexer/src/lib/tokenscope/seaport.ts @@ -201,13 +201,13 @@ export const getSupportedSaleFromOrderFulfilledEvent = ( if ( consolidatedOfferNFT && !consolidatedConsiderationNFT && - consolidatedOfferPayment && - !consolidatedConsiderationPayment + !consolidatedOfferPayment && + consolidatedConsiderationPayment ) { return { orderHash, nft: consolidatedOfferNFT, - payment: consolidatedOfferPayment, + payment: consolidatedConsiderationPayment, seller: offerer, buyer: recipient, }; @@ -218,13 +218,13 @@ export const getSupportedSaleFromOrderFulfilledEvent = ( if ( !consolidatedOfferNFT && consolidatedConsiderationNFT && - !consolidatedOfferPayment && - consolidatedConsiderationPayment + consolidatedOfferPayment && + !consolidatedConsiderationPayment ) { return { orderHash, nft: consolidatedConsiderationNFT, - payment: consolidatedConsiderationPayment, + payment: consolidatedOfferPayment, seller: recipient, buyer: offerer, }; From 3dfcc2c9d0fd91a0c1483a86ecb618fd23988877 Mon Sep 17 00:00:00 2001 From: shrugs Date: Tue, 26 Aug 2025 12:58:46 -0500 Subject: [PATCH 60/61] fix: update docstring --- apps/ensindexer/src/lib/currencies.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/apps/ensindexer/src/lib/currencies.ts b/apps/ensindexer/src/lib/currencies.ts index 93da4917f..52c5134a7 100644 --- a/apps/ensindexer/src/lib/currencies.ts +++ b/apps/ensindexer/src/lib/currencies.ts @@ -129,10 +129,8 @@ const getSupportedCurrencyContractsForChain = (chainId: ChainId): Record Date: Tue, 26 Aug 2025 13:04:53 -0500 Subject: [PATCH 61/61] fix: do not enable tokenscope just yet --- .../src/content/docs/docs/usage/hosted-ensnode-instances.mdx | 4 ++-- terraform/main.tf | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/ensnode.io/src/content/docs/docs/usage/hosted-ensnode-instances.mdx b/docs/ensnode.io/src/content/docs/docs/usage/hosted-ensnode-instances.mdx index c1418fbf2..bd29f43a0 100644 --- a/docs/ensnode.io/src/content/docs/docs/usage/hosted-ensnode-instances.mdx +++ b/docs/ensnode.io/src/content/docs/docs/usage/hosted-ensnode-instances.mdx @@ -30,7 +30,7 @@ These ENSNode instances focus on pushing the boundaries of what's possible with instanceURL="https://api.alpha.ensnode.io" connectWithENSAdminURL="https://admin.ensnode.io/connect?ensnode=https%3A%2F%2Fapi.alpha.ensnode.io" namespace="mainnet" - plugins="subgraph, basenames, lineanames, threedns, reverse-resolvers, referrals, tokenscope" + plugins="subgraph, basenames, lineanames, threedns, reverse-resolvers, referrals" purpose="Demonstration of ENSNode's multichain ENS indexing capability for the ENS namespace rooted on Mainnet. Provides a superset of the data indexed by the ENS Subgraph. Offers a preview of ENS Protocol Acceleration." /> @@ -39,7 +39,7 @@ These ENSNode instances focus on pushing the boundaries of what's possible with instanceURL="https://api.alpha-sepolia.ensnode.io" connectWithENSAdminURL="https://admin.ensnode.io/connect?ensnode=https%3A%2F%2Fapi.alpha-sepolia.ensnode.io" namespace="sepolia" - plugins="subgraph, basenames, lineanames, reverse-resolvers, referrals, tokenscope" + plugins="subgraph, basenames, lineanames, reverse-resolvers, referrals" purpose="Demonstration of ENSNode's multichain ENS indexing capability for the ENS namespace rooted on Sepolia. Provides a superset of the data indexed by the ENS Subgraph. Offers a preview of ENS Protocol Acceleration." /> diff --git a/terraform/main.tf b/terraform/main.tf index 315494910..9aa0f871b 100644 --- a/terraform/main.tf +++ b/terraform/main.tf @@ -40,7 +40,7 @@ locals { instance_name = "alpha" subdomain_prefix = "alpha.${var.render_environment}" database_schema = "alphaSchema-${var.ensnode_version}" - plugins = "subgraph,basenames,lineanames,threedns,reverse-resolvers,referrals,tokenscope" + plugins = "subgraph,basenames,lineanames,threedns,reverse-resolvers,referrals" namespace = "mainnet" heal_reverse_addresses = "true" index_additional_resolver_records = "true" @@ -51,7 +51,7 @@ locals { instance_name = "alpha-sepolia" subdomain_prefix = "alpha-sepolia.${var.render_environment}" database_schema = "alphaSepoliaSchema-${var.ensnode_version}" - plugins = "subgraph,basenames,lineanames,reverse-resolvers,referrals,tokenscope" + plugins = "subgraph,basenames,lineanames,reverse-resolvers,referrals" namespace = "sepolia" heal_reverse_addresses = "true" index_additional_resolver_records = "true"