From 58e6ab00b02f65d3b7ea065e4b5aecd1db55b739 Mon Sep 17 00:00:00 2001 From: Zimtente Date: Mon, 14 Jul 2025 08:32:57 +0200 Subject: [PATCH 1/4] 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 2/4] 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 3/4] 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 4/4] 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.