diff --git a/.changeset/hca-aware-events.md b/.changeset/hca-aware-events.md new file mode 100644 index 000000000..4364097b0 --- /dev/null +++ b/.changeset/hca-aware-events.md @@ -0,0 +1,5 @@ +--- +"ensapi": minor +--- + +Adds HCA-aware `Event.sender` to the Omnigraph API alongside the existing `Event.from`. For ENSv2 events that emit an explicit `sender`/`owner`/`account`/ERC1155 `operator` argument, `Event.sender` is set from that argument (the HCA Smart Account address, if used) and falls back to `tx.from` otherwise. Adds a `sender` filter to `EventsWhereInput`. `Account.events` now filters by `sender` (HCA-aware) instead of `tx.from`. Documents HCA-aware semantics on `Domain.owner`, `Registration.registrant`/`unregistrant`, and `*.PermissionsUser.user`. diff --git a/apps/ensapi/src/lib/resolution/execute-operations.ts b/apps/ensapi/src/lib/resolution/execute-operations.ts index 9d4381624..8c87e82a3 100644 --- a/apps/ensapi/src/lib/resolution/execute-operations.ts +++ b/apps/ensapi/src/lib/resolution/execute-operations.ts @@ -6,6 +6,7 @@ import { type Hex, type Name, type RecordVersion, + toNormalizedAddress, } from "enssdk"; import { ContractFunctionExecutionError, @@ -161,6 +162,6 @@ export function interpretOperationWithRawResult(call: Operation, raw: unknown): return { ...call, result: size(data) === 0 ? null : { contentType, data } }; } case "interfaceImplementer": - return { ...call, result: interpretAddress(raw as Address) }; + return { ...call, result: interpretAddress(toNormalizedAddress(raw as string)) }; } } diff --git a/apps/ensapi/src/omnigraph-api/lib/find-events/find-events-resolver.ts b/apps/ensapi/src/omnigraph-api/lib/find-events/find-events-resolver.ts index bec3513c8..f14bb692a 100644 --- a/apps/ensapi/src/omnigraph-api/lib/find-events/find-events-resolver.ts +++ b/apps/ensapi/src/omnigraph-api/lib/find-events/find-events-resolver.ts @@ -26,8 +26,10 @@ interface EventsWhere { timestamp_gte?: bigint | null; /** Filter to events at or before this timestamp. */ timestamp_lte?: bigint | null; - /** Filter to events sent by this address. */ + /** Filter to events whose `tx.from` matches. Not HCA-aware. */ from?: Address | null; + /** Filter to events whose HCA-aware `sender` matches. */ + sender?: Address | null; } /** @@ -49,6 +51,7 @@ function eventsWhereConditions(where?: EventsWhere | null): SQL | undefined { ? lte(ensIndexerSchema.event.timestamp, where.timestamp_lte) : undefined, where.from ? eq(ensIndexerSchema.event.from, where.from) : undefined, + where.sender ? eq(ensIndexerSchema.event.sender, where.sender) : undefined, ); } diff --git a/apps/ensapi/src/omnigraph-api/schema/account.integration.test.ts b/apps/ensapi/src/omnigraph-api/schema/account.integration.test.ts index b3f14f8c9..0e35b228e 100644 --- a/apps/ensapi/src/omnigraph-api/schema/account.integration.test.ts +++ b/apps/ensapi/src/omnigraph-api/schema/account.integration.test.ts @@ -99,7 +99,7 @@ describe("Account.events", () => { expect(events.length).toBeGreaterThan(0); for (const event of events) { - expect(event.from).toBe(DEVNET_DEPLOYER); + expect(event.sender).toBe(DEVNET_DEPLOYER); } }); }); diff --git a/apps/ensapi/src/omnigraph-api/schema/account.ts b/apps/ensapi/src/omnigraph-api/schema/account.ts index 55a4d0736..08c83d6d2 100644 --- a/apps/ensapi/src/omnigraph-api/schema/account.ts +++ b/apps/ensapi/src/omnigraph-api/schema/account.ts @@ -90,13 +90,14 @@ AccountRef.implement({ // Account.events ////////////////// events: t.connection({ - description: "All Events for which this Account is the sender (i.e. `Transaction.from`).", + description: + "All Events for which this Account is the HCA-aware `sender` (i.e. `Event.sender`).", type: EventRef, args: { where: t.arg({ type: AccountEventsWhereInput }), }, resolve: (parent, args) => - resolveFindEvents({ ...args, where: { ...args.where, from: parent.id } }), + resolveFindEvents({ ...args, where: { ...args.where, sender: parent.id } }), }), /////////////////////// diff --git a/apps/ensapi/src/omnigraph-api/schema/domain.ts b/apps/ensapi/src/omnigraph-api/schema/domain.ts index ea0e147fc..915ec9c1b 100644 --- a/apps/ensapi/src/omnigraph-api/schema/domain.ts +++ b/apps/ensapi/src/omnigraph-api/schema/domain.ts @@ -177,7 +177,8 @@ DomainInterfaceRef.implement({ //////////////// owner: t.field({ type: AccountRef, - description: "The owner of this Domain.", + description: + "If this is an ENSv1Domain, this is the effective owner of the Domain. If this is an ENSv2Domain, this is the on-chain owner address (the HCA account address if used).", nullable: true, resolve: (parent) => parent.ownerId, }), diff --git a/apps/ensapi/src/omnigraph-api/schema/event.ts b/apps/ensapi/src/omnigraph-api/schema/event.ts index 99a9702c7..7e20923bf 100644 --- a/apps/ensapi/src/omnigraph-api/schema/event.ts +++ b/apps/ensapi/src/omnigraph-api/schema/event.ts @@ -95,12 +95,23 @@ EventRef.implement({ // Event.from ////////////// from: t.field({ - description: "Identifies the sender of the Transaction within which this Event was emitted.", + description: + "Identifies the sender of the Transaction within which this Event was emitted (`tx.from`). Never HCA-aware — always the EOA/relayer that submitted the transaction. Use `Event.sender` for the HCA-aware actor.", type: "Address", nullable: false, resolve: (parent) => parent.from, }), + //////////////// + // Event.sender + //////////////// + sender: t.field({ + description: "The HCA account address if used, otherwise Transaction.from.", + type: "Address", + nullable: false, + resolve: (parent) => parent.sender, + }), + //////////// // Event.to //////////// @@ -160,7 +171,7 @@ EventRef.implement({ /** * Shared filter for events connections. Used by Domain.events, Resolver.events, Permissions.events, - * and Account.events (which excludes `from` since it's implied). + * and Account.events (which excludes `sender` since it's implied). */ export const EventsWhereInput = builder.inputType("EventsWhereInput", { description: "Filter conditions for an events connection.", @@ -180,16 +191,22 @@ export const EventsWhereInput = builder.inputType("EventsWhereInput", { }), from: t.field({ type: "Address", - description: "Filter to events sent by this address.", + description: + "Filter to events whose `tx.from` matches. Not HCA-aware — use `sender` to filter by the HCA account address.", + }), + sender: t.field({ + type: "Address", + description: + "Filter to events whose `sender` matches: the HCA account address if used, otherwise Transaction.from.", }), }), }); /** - * Like EventsWhereInput but without `from` (used where `from` is implied, e.g. Account.events). + * Like EventsWhereInput but without `sender` (used where `sender` is implied, e.g. Account.events). */ export const AccountEventsWhereInput = builder.inputType("AccountEventsWhereInput", { - description: "Filter conditions for Account.events (where `from` is implied by the Account).", + description: "Filter conditions for Account.events (where `sender` is implied by the Account).", fields: (t) => ({ selector_in: t.field({ type: ["Hex"], @@ -204,5 +221,10 @@ export const AccountEventsWhereInput = builder.inputType("AccountEventsWhereInpu type: "BigInt", description: "Filter to events at or before this UnixTimestamp.", }), + from: t.field({ + type: "Address", + description: + "Filter to events whose `tx.from` matches. Not HCA-aware — the Account's HCA-aware filter is applied via `sender = Account.id`.", + }), }), }); diff --git a/apps/ensapi/src/omnigraph-api/schema/permissions.ts b/apps/ensapi/src/omnigraph-api/schema/permissions.ts index fda7f12a3..de6a5bd90 100644 --- a/apps/ensapi/src/omnigraph-api/schema/permissions.ts +++ b/apps/ensapi/src/omnigraph-api/schema/permissions.ts @@ -260,7 +260,8 @@ PermissionsUserRef.implement({ // PermissionsUser.user //////////////////////// user: t.field({ - description: "The User for whom these Roles are granted.", + description: + "The user/grantee address this Permission is granted to (the HCA account address if used).", type: AccountRef, nullable: false, resolve: (parent) => parent.user, diff --git a/apps/ensapi/src/omnigraph-api/schema/registration.ts b/apps/ensapi/src/omnigraph-api/schema/registration.ts index a0a53b436..588e1c4a6 100644 --- a/apps/ensapi/src/omnigraph-api/schema/registration.ts +++ b/apps/ensapi/src/omnigraph-api/schema/registration.ts @@ -140,7 +140,8 @@ RegistrationInterfaceRef.implement({ // Registration.registrant /////////////////////////// registrant: t.field({ - description: "The Registrant of a Registration, if exists.", + description: + "The Registrant of a Registration, if exists. For ENSv2 Registrations, the protocol-emitted registrant address (the HCA account address if used).", type: AccountRef, nullable: true, resolve: (parent) => parent.registrantId, @@ -150,7 +151,8 @@ RegistrationInterfaceRef.implement({ // Registration.unregistrant ///////////////////////////// unregistrant: t.field({ - description: "The Unregistrant of a Registration, if exists.", + description: + "The Unregistrant of a Registration, if exists. For ENSv2 Registrations, the protocol-emitted unregistrant address (the HCA account address if used).", type: AccountRef, nullable: true, resolve: (parent) => parent.unregistrantId, diff --git a/apps/ensapi/src/omnigraph-api/schema/registry-permissions-user.ts b/apps/ensapi/src/omnigraph-api/schema/registry-permissions-user.ts index 1edf94f81..b5d7f0bc6 100644 --- a/apps/ensapi/src/omnigraph-api/schema/registry-permissions-user.ts +++ b/apps/ensapi/src/omnigraph-api/schema/registry-permissions-user.ts @@ -49,7 +49,8 @@ RegistryPermissionsUserRef.implement({ // RegistryPermissionsUser.user ///////////////////////////////// user: t.field({ - description: "The User for whom these Roles are granted.", + description: + "The user/grantee address this Permission is granted to (the HCA account address if used).", type: AccountRef, nullable: false, resolve: (parent) => parent.user, diff --git a/apps/ensapi/src/omnigraph-api/schema/resolver-permissions-user.ts b/apps/ensapi/src/omnigraph-api/schema/resolver-permissions-user.ts index 71a6bd81d..3221d7ec5 100644 --- a/apps/ensapi/src/omnigraph-api/schema/resolver-permissions-user.ts +++ b/apps/ensapi/src/omnigraph-api/schema/resolver-permissions-user.ts @@ -49,7 +49,8 @@ ResolverPermissionsUserRef.implement({ // ResolverPermissionsUser.user ////////////////////////////////// user: t.field({ - description: "The User for whom these Roles are granted.", + description: + "The user/grantee address this Permission is granted to (the HCA account address if used).", type: AccountRef, nullable: false, resolve: (parent) => parent.user, diff --git a/apps/ensapi/src/test/integration/find-events/event-pagination-queries.ts b/apps/ensapi/src/test/integration/find-events/event-pagination-queries.ts index 265515ec7..e7fc735cf 100644 --- a/apps/ensapi/src/test/integration/find-events/event-pagination-queries.ts +++ b/apps/ensapi/src/test/integration/find-events/event-pagination-queries.ts @@ -21,6 +21,7 @@ export const EventFragment = gql` transactionHash transactionIndex from + sender to address logIndex @@ -38,6 +39,7 @@ export type EventResult = { transactionHash: Hex; transactionIndex: number; from: NormalizedAddress; + sender: NormalizedAddress; to: NormalizedAddress | null; address: NormalizedAddress; logIndex: number; diff --git a/apps/ensindexer/src/lib/ensv2/account-db-helpers.ts b/apps/ensindexer/src/lib/ensv2/account-db-helpers.ts index 0209813ac..ac55983c4 100644 --- a/apps/ensindexer/src/lib/ensv2/account-db-helpers.ts +++ b/apps/ensindexer/src/lib/ensv2/account-db-helpers.ts @@ -1,4 +1,4 @@ -import type { Address } from "enssdk"; +import type { NormalizedAddress } from "enssdk"; import { interpretAddress } from "@ensnode/ensnode-sdk"; @@ -8,12 +8,14 @@ import { ensIndexerSchema, type IndexingEngineContext } from "@/lib/indexing-eng * Ensures that the account identified by `address` exists. * If `address` is the zeroAddress, no-op. */ -export async function ensureAccount(context: IndexingEngineContext, address: Address) { - const interpreted = interpretAddress(address); - if (interpreted === null) return; +export async function ensureAccount( + context: IndexingEngineContext, + address: NormalizedAddress, +): Promise { + const id = interpretAddress(address); + if (id === null) return null; - await context.ensDb - .insert(ensIndexerSchema.account) - .values({ id: interpreted }) - .onConflictDoNothing(); + await context.ensDb.insert(ensIndexerSchema.account).values({ id: id }).onConflictDoNothing(); + + return id; } diff --git a/apps/ensindexer/src/lib/ensv2/event-db-helpers.ts b/apps/ensindexer/src/lib/ensv2/event-db-helpers.ts index ac7c99aa2..21d5abfbd 100644 --- a/apps/ensindexer/src/lib/ensv2/event-db-helpers.ts +++ b/apps/ensindexer/src/lib/ensv2/event-db-helpers.ts @@ -3,6 +3,7 @@ import { type DomainId, makePermissionsId, makeResolverId, + type NormalizedAddress, type PermissionsUserId, } from "enssdk"; import type { Hash } from "viem"; @@ -24,7 +25,11 @@ const hasTopics = (topics: LogEventBase["log"]["topics"]): topics is Topics => * * @returns event.id */ -export async function ensureEvent(context: IndexingEngineContext, event: LogEventBase) { +export async function ensureEvent( + context: IndexingEngineContext, + event: LogEventBase, + sender?: NormalizedAddress | null, +) { // all relevant ENS events obviously have a topic, so we can safely constrain the type of this data if (!hasTopics(event.log.topics)) { throw new Error(`Invariant: All events indexed via ensureEvent must have at least one topic.`); @@ -39,6 +44,9 @@ export async function ensureEvent(context: IndexingEngineContext, event: LogEven .values({ id: event.id, + // sender override if provided, otherwise transaction.from + sender: sender ?? event.transaction.from, + // chain chainId: context.chain.id, diff --git a/apps/ensindexer/src/plugins/ensv2/handlers/ensv2/ENSv2Registry.ts b/apps/ensindexer/src/plugins/ensv2/handlers/ensv2/ENSv2Registry.ts index 170826f2f..480091007 100644 --- a/apps/ensindexer/src/plugins/ensv2/handlers/ensv2/ENSv2Registry.ts +++ b/apps/ensindexer/src/plugins/ensv2/handlers/ensv2/ENSv2Registry.ts @@ -128,14 +128,14 @@ export default function () { .onConflictDoUpdate({ tokenId }); // insert Registration - const eventId = await ensureEvent(context, event); - await ensureAccount(context, registrant); + const registrantId = await ensureAccount(context, registrant); + const eventId = await ensureEvent(context, event, registrantId); await insertLatestRegistration(context, { domainId, type: isReservation ? "ENSv2RegistryReservation" : "ENSv2RegistryRegistration", registrarChainId: registry.chainId, registrarAddress: registry.address, - registrantId: interpretAddress(registrant), + registrantId, start: event.block.timestamp, expiry, eventId, @@ -189,17 +189,17 @@ export default function () { // unregistering a label just immediately sets its expiration to event.block.timestamp, which // effectively removes it from resolution (which interprets expired names as non-existent) - await ensureAccount(context, unregistrant); + const unregistrantId = await ensureAccount(context, unregistrant); await context.ensDb.update(ensIndexerSchema.registration, { id: registration.id }).set({ expiry: event.block.timestamp, - unregistrantId: interpretAddress(unregistrant), + unregistrantId, }); // NOTE(shrugs): PermissionedRegistry also increments eacVersionId and tokenVersionId if there was a // previous owner, but i'm not sure if we need to handle that detail here // push event to domain history - const eventId = await ensureEvent(context, event); + const eventId = await ensureEvent(context, event, unregistrantId); await ensureDomainEvent(context, domainId, eventId); }, ); @@ -217,7 +217,6 @@ export default function () { sender: NormalizedAddress; }>; }) => { - // biome-ignore lint/correctness/noUnusedVariables: not sure if we care to index sender const { tokenId, newExpiry: expiry, sender } = event.args; const registry = getThisAccountId(context, event); @@ -244,7 +243,8 @@ export default function () { .set({ expiry }); // push event to domain history - const eventId = await ensureEvent(context, event); + const senderId = await ensureAccount(context, sender); + const eventId = await ensureEvent(context, event, senderId); await ensureDomainEvent(context, domainId, eventId); }, ); @@ -259,9 +259,10 @@ export default function () { event: EventWithArgs<{ tokenId: TokenId; subregistry: NormalizedAddress; + sender: NormalizedAddress; }>; }) => { - const { tokenId, subregistry: _subregistry } = event.args; + const { tokenId, subregistry: _subregistry, sender } = event.args; const subregistry = interpretAddress(_subregistry); const registryAccountId = getThisAccountId(context, event); @@ -301,7 +302,8 @@ export default function () { } // push event to domain history - const eventId = await ensureEvent(context, event); + const senderId = await ensureAccount(context, sender); + const eventId = await ensureEvent(context, event, senderId); await ensureDomainEvent(context, domainId, eventId); }, ); @@ -344,9 +346,9 @@ export default function () { event, }: { context: IndexingEngineContext; - event: EventWithArgs<{ id: TokenId; to: NormalizedAddress }>; + event: EventWithArgs<{ id: TokenId; to: NormalizedAddress; operator: NormalizedAddress }>; }) { - const { id: tokenId, to: owner } = event.args; + const { id: tokenId, to: owner, operator } = event.args; const storageId = makeStorageId(tokenId); const registry = getThisAccountId(context, event); @@ -358,12 +360,12 @@ export default function () { if (!exists) return; // no-op non-Registry ERC1155 Transfers // update the Domain's ownerId - await context.ensDb - .update(ensIndexerSchema.domain, { id: domainId }) - .set({ ownerId: interpretAddress(owner) }); + const ownerId = await ensureAccount(context, owner); + await context.ensDb.update(ensIndexerSchema.domain, { id: domainId }).set({ ownerId }); // push event to domain history - const eventId = await ensureEvent(context, event); + const operatorId = await ensureAccount(context, operator); + const eventId = await ensureEvent(context, event, operatorId); await ensureDomainEvent(context, domainId, eventId); } diff --git a/apps/ensindexer/src/plugins/ensv2/handlers/ensv2/ETHRegistrar.ts b/apps/ensindexer/src/plugins/ensv2/handlers/ensv2/ETHRegistrar.ts index 1a8ec7ded..171a368b9 100644 --- a/apps/ensindexer/src/plugins/ensv2/handlers/ensv2/ETHRegistrar.ts +++ b/apps/ensindexer/src/plugins/ensv2/handlers/ensv2/ETHRegistrar.ts @@ -11,7 +11,6 @@ import { import { type EncodedReferrer, - interpretAddress, isRegistrationFullyExpired, PluginName, toJson, @@ -103,12 +102,11 @@ export default function () { } // upsert registrant - await ensureAccount(context, owner); + const registrantId = await ensureAccount(context, owner); // update latest Registration await context.ensDb.update(ensIndexerSchema.registration, { id: registration.id }).set({ - // TODO: reconsider 'Registration.registrant' if ENSv2 doesn't provide explicit 'registrant' - registrantId: interpretAddress(owner), + registrantId, // we now know the correct registrar to attribute to, so overwrite registrarChainId: registrar.chainId, @@ -122,7 +120,7 @@ export default function () { }); // push event to domain history - const eventId = await ensureEvent(context, event); + const eventId = await ensureEvent(context, event, registrantId); await ensureDomainEvent(context, domainId, eventId); }, ); diff --git a/docs/ensnode.io/src/content/docs/ensdb/concepts/database-schemas.mdx b/docs/ensnode.io/src/content/docs/ensdb/concepts/database-schemas.mdx index 965a031ce..1720691ba 100644 --- a/docs/ensnode.io/src/content/docs/ensdb/concepts/database-schemas.mdx +++ b/docs/ensnode.io/src/content/docs/ensdb/concepts/database-schemas.mdx @@ -160,7 +160,8 @@ the Event responsible for its existence. | `timestamp` | `numeric(78)` | no | Block timestamp. | | `transactionHash` | `text` | no | Transaction hash. | | `transactionIndex` | `integer` | no | Index of the transaction within the block. | -| `from` | `text` | no | Transaction sender address. | +| `from` | `text` | no | Transaction sender address (`tx.from`). Never HCA-aware — always the EOA/relayer that submitted the transaction. Use `sender` for the HCA-aware actor. | +| `sender` | `text` | no | The HCA account address if used, otherwise `Transaction.from`. For ENSv2 events that emit an explicit `sender` / `owner` / `account` argument, this is set from that argument. For all other events (and all ENSv1 events), this falls back to `from` (i.e. `tx.from`). | | `to` | `text` | yes | Transaction recipient address. A `null` value means this was a contract-deployment transaction. | | `address` | `text` | no | Address of the contract that emitted the log. | | `logIndex` | `integer` | no | Index of the log within the transaction. | @@ -168,7 +169,7 @@ the Event responsible for its existence. | `topics` | `text[]` | no | All log topics. | | `data` | `text` | no | Log data. | -**Indexes:** `selector`, `from`, `timestamp`. +**Indexes:** `selector`, `from`, `sender`, `timestamp`. #### `domain_events` @@ -203,6 +204,17 @@ Join table linking a `permissions` record to its associated `events`. **Primary key:** `(permissionsId, eventId)`. +#### `permissions_user_events` + +Join table linking a `permissions_users` record to its associated `events` — i.e. the per-`(contract, resource, user)` history of role grants, revokes, and bitmap mutations. + +| Column | Type | Nullable | +|--------|------|----------| +| `permissionsUserId` | `text` | no | +| `eventId` | `text` | no | + +**Primary key:** `(permissionsUserId, eventId)`. + #### `accounts` | Column | Type | Nullable | Description | @@ -244,7 +256,7 @@ Domain-Resolver relations are tracked via the Protocol Acceleration plugin, not | `tokenId` | `numeric(78)` | yes | ENSv2 only: the TokenId within the ENSv2Registry. `null` for ENSv1 domains. | | `node` | `text` | yes | ENSv1 only: the domain's namehash. `null` for ENSv2 domains. | | `labelHash` | `text` | no | Represents a labelHash. References `labels.labelHash`. | -| `ownerId` | `text` | yes | Materialized effective owner address. | +| `ownerId` | `text` | yes | If `ENSv1Domain`, the materialized effective owner address. If `ENSv2Domain`, the on-chain owner address (the HCA account address if used). | | `rootRegistryOwnerId` | `text` | yes | ENSv1 only: the owner recorded in the root ENSv1 registry. `null` for ENSv2 domains. | **Indexes:** `type`, `registryId`, `subregistryId` (partial: non-null only), `ownerId`, `labelHash`. @@ -279,8 +291,8 @@ A registration is keyed by `id`. | `gracePeriod` | `numeric(78)` | yes | Grace period duration in seconds. `BaseRegistrar` only. | | `registrarChainId` | `integer` | no | Chain of the registrar contract. | | `registrarAddress` | `text` | no | Address of the registrar contract. | -| `registrantId` | `text` | yes | Account that initiated the registration. | -| `unregistrantId` | `text` | yes | Account that triggered an unregistration, if applicable. | +| `registrantId` | `text` | yes | Account that initiated the registration. For ENSv2 Registrations, the protocol-emitted registrant address (the HCA account address if used). | +| `unregistrantId` | `text` | yes | Account that triggered an unregistration, if applicable. For ENSv2 Registrations, the protocol-emitted unregistrant address (the HCA account address if used). | | `referrer` | `text` | yes | Encoded referrer value emitted at registration time. | | `fuses` | `integer` | yes | Fuse bitmap. `NameWrapper` and wrapped `BaseRegistrar` only. | | `base` | `numeric(78)` | yes | Base registration cost in wei. `BaseRegistrar` and `ENSv2Registrar` only. | @@ -374,7 +386,7 @@ A user's role bitmap for a specific resource within a `permissions` contract. | `chainId` | `integer` | no | Chain of the parent permissions contract. | | `address` | `text` | no | Address of the parent permissions contract. | | `resource` | `numeric(78)` | no | Resource identifier. | -| `user` | `text` | no | The user's Ethereum address. | +| `user` | `text` | no | The user/grantee address this Permission is granted to (the HCA account address if used). | | `roles` | `numeric(78)` | no | Roles bitmap for this user on this resource. | **Indexes:** unique on `(chainId, address, resource, user)`. diff --git a/packages/ensdb-sdk/src/ensindexer-abstract/ensv2.schema.ts b/packages/ensdb-sdk/src/ensindexer-abstract/ensv2.schema.ts index 543f7232e..0308fdeab 100644 --- a/packages/ensdb-sdk/src/ensindexer-abstract/ensv2.schema.ts +++ b/packages/ensdb-sdk/src/ensindexer-abstract/ensv2.schema.ts @@ -1,10 +1,10 @@ import type { - Address, ChainId, DomainId, InterpretedLabel, LabelHash, Node, + NormalizedAddress, PermissionsId, PermissionsResourceId, PermissionsUserId, @@ -96,6 +96,9 @@ export const event = onchainTable( // Ponder's event.id id: t.text().primaryKey(), + // The HCA account address if used, otherwise Transaction.from. + sender: t.hex().notNull().$type(), + // Event Log Metadata // chain @@ -109,11 +112,13 @@ export const event = onchainTable( // transaction transactionHash: t.hex().notNull().$type(), transactionIndex: t.integer().notNull(), - from: t.hex().notNull().$type
(), - to: t.hex().$type
(), // NOTE: a null `to` means this was a tx that deployed a contract + // `tx.from` — never HCA-aware. Always the EOA/relayer that submitted the transaction. + // Use `event.sender` for the HCA-aware actor. + from: t.hex().notNull().$type(), + to: t.hex().$type(), // NOTE: a null `to` means this was a tx that deployed a contract // log - address: t.hex().notNull().$type
(), + address: t.hex().notNull().$type(), logIndex: t.integer().notNull().$type(), selector: t.hex().notNull().$type(), topics: t.hex().array().notNull().$type<[Hash, ...Hash[]]>(), @@ -122,6 +127,7 @@ export const event = onchainTable( (t) => ({ bySelector: index().on(t.selector), byFrom: index().on(t.from), + bySender: index().on(t.sender), byTimestamp: index().on(t.timestamp), }), ); @@ -167,7 +173,7 @@ export const permissionsUserEvent = onchainTable( /////////// export const account = onchainTable("accounts", (t) => ({ - id: t.hex().primaryKey().$type
(), + id: t.hex().primaryKey().$type(), })); export const account_relations = relations(account, ({ many }) => ({ @@ -196,7 +202,7 @@ export const registry = onchainTable( type: registryType().notNull(), chainId: t.integer().notNull().$type(), - address: t.hex().notNull().$type
(), + address: t.hex().notNull().$type(), // If this is an ENSv1VirtualRegistry, `node` is the namehash of the parent ENSv1 domain that // owns it, otherwise null. @@ -250,11 +256,12 @@ export const domain = onchainTable( // represents a labelHash labelHash: t.hex().notNull().$type(), - // may have an owner - ownerId: t.hex().$type
(), + // If this is an ENSv1Domain, this is the effective owner of the Domain. + // If this is an ENSv2Domain, this is the on-chain owner address (the HCA account address if used). + ownerId: t.hex().$type(), // If this is an ENSv1Domain, may have a `rootRegistryOwner`, otherwise null. - rootRegistryOwnerId: t.hex().$type
(), + rootRegistryOwnerId: t.hex().$type(), // NOTE: Domain-Resolver Relations tracked via Protocol Acceleration plugin // NOTE: parent is derived via registryCanonicalDomain, not stored on the domain row @@ -331,13 +338,15 @@ export const registration = onchainTable( // registrar AccountId registrarChainId: t.integer().notNull().$type(), - registrarAddress: t.hex().notNull().$type
(), + registrarAddress: t.hex().notNull().$type(), - // may reference a registrant - registrantId: t.hex().$type
(), + // may reference a registrant. If this is an ENSv2 Registration, the protocol-emitted + // registrant address (the HCA account address if used). + registrantId: t.hex().$type(), - // may reference an unregistrant - unregistrantId: t.hex().$type
(), + // may reference an unregistrant. If this is an ENSv2 Registration, the protocol-emitted + // unregistrant address (the HCA account address if used). + unregistrantId: t.hex().$type(), // may have referrer data referrer: t.hex().$type(), @@ -492,7 +501,7 @@ export const permissions = onchainTable( id: t.text().primaryKey().$type(), chainId: t.integer().notNull().$type(), - address: t.hex().notNull().$type
(), + address: t.hex().notNull().$type(), }), (t) => ({ byId: uniqueIndex().on(t.chainId, t.address), @@ -510,7 +519,7 @@ export const permissionsResource = onchainTable( id: t.text().primaryKey().$type(), chainId: t.integer().notNull().$type(), - address: t.hex().notNull().$type
(), + address: t.hex().notNull().$type(), resource: t.bigint().notNull(), }), (t) => ({ @@ -531,9 +540,10 @@ export const permissionsUser = onchainTable( id: t.text().primaryKey().$type(), chainId: t.integer().notNull().$type(), - address: t.hex().notNull().$type
(), + address: t.hex().notNull().$type(), resource: t.bigint().notNull(), - user: t.hex().notNull().$type
(), + // The user/grantee address this Permission is granted to (the HCA account address if used). + user: t.hex().notNull().$type(), // has one roles bitmap roles: t.bigint().notNull(), diff --git a/packages/ensnode-sdk/src/shared/interpretation/interpret-address.ts b/packages/ensnode-sdk/src/shared/interpretation/interpret-address.ts index 9fd9068a1..6f2737928 100644 --- a/packages/ensnode-sdk/src/shared/interpretation/interpret-address.ts +++ b/packages/ensnode-sdk/src/shared/interpretation/interpret-address.ts @@ -1,8 +1,8 @@ -import type { Address } from "enssdk"; +import type { NormalizedAddress } from "enssdk"; import { isAddressEqual, zeroAddress } from "viem"; /** - * Interprets a viem#Address. zeroAddress is interpreted as null, otherwise Address. + * Interprets a NormalizedAddress. zeroAddress is interpreted as null, otherwise as-is. */ -export const interpretAddress = (owner: Address) => +export const interpretAddress = (owner: NormalizedAddress): NormalizedAddress | null => isAddressEqual(zeroAddress, owner) ? null : owner; diff --git a/packages/enssdk/src/omnigraph/generated/introspection.ts b/packages/enssdk/src/omnigraph/generated/introspection.ts index c6763e36a..b018ad6b8 100644 --- a/packages/enssdk/src/omnigraph/generated/introspection.ts +++ b/packages/enssdk/src/omnigraph/generated/introspection.ts @@ -486,6 +486,13 @@ const introspection = { "kind": "INPUT_OBJECT", "name": "AccountEventsWhereInput", "inputFields": [ + { + "name": "from", + "type": { + "kind": "SCALAR", + "name": "Address" + } + }, { "name": "selector_in", "type": { @@ -3088,6 +3095,18 @@ const introspection = { "args": [], "isDeprecated": false }, + { + "name": "sender", + "type": { + "kind": "NON_NULL", + "ofType": { + "kind": "SCALAR", + "name": "Address" + } + }, + "args": [], + "isDeprecated": false + }, { "name": "timestamp", "type": { @@ -3178,6 +3197,13 @@ const introspection = { } } }, + { + "name": "sender", + "type": { + "kind": "SCALAR", + "name": "Address" + } + }, { "name": "timestamp_gte", "type": { diff --git a/packages/enssdk/src/omnigraph/generated/schema.graphql b/packages/enssdk/src/omnigraph/generated/schema.graphql index ac546669a..dcb12e7d1 100644 --- a/packages/enssdk/src/omnigraph/generated/schema.graphql +++ b/packages/enssdk/src/omnigraph/generated/schema.graphql @@ -7,7 +7,7 @@ type Account { domains(after: String, before: String, first: Int, last: Int, order: DomainsOrderInput, where: AccountDomainsWhereInput): AccountDomainsConnection """ - All Events for which this Account is the sender (i.e. `Transaction.from`). + All Events for which this Account is the HCA-aware `sender` (i.e. `Event.sender`). """ events(after: String, before: String, first: Int, last: Int, where: AccountEventsWhereInput): AccountEventsConnection @@ -68,9 +68,14 @@ type AccountEventsConnectionEdge { } """ -Filter conditions for Account.events (where `from` is implied by the Account). +Filter conditions for Account.events (where `sender` is implied by the Account). """ input AccountEventsWhereInput { + """ + Filter to events whose `tx.from` matches. Not HCA-aware — the Account's HCA-aware filter is applied via `sender = Account.id`. + """ + from: Address + """ Filter to events whose selector (event signature) is one of the provided values. """ @@ -166,7 +171,9 @@ type BaseRegistrarRegistration implements Registration { """The extra `referrer` data provided with a Registration, if exists.""" referrer: Hex - """The Registrant of a Registration, if exists.""" + """ + The Registrant of a Registration, if exists. For ENSv2 Registrations, the protocol-emitted registrant address (the HCA account address if used). + """ registrant: Account """The Registrar contract under which this Registration is managed.""" @@ -180,7 +187,9 @@ type BaseRegistrarRegistration implements Registration { """A UnixTimestamp indicating when this Registration was created.""" start: BigInt! - """The Unregistrant of a Registration, if exists.""" + """ + The Unregistrant of a Registration, if exists. For ENSv2 Registrations, the protocol-emitted unregistrant address (the HCA account address if used). + """ unregistrant: Account """ @@ -216,7 +225,9 @@ interface Domain { """ name: InterpretedName - """The owner of this Domain.""" + """ + If this is an ENSv1Domain, this is the effective owner of the Domain. If this is an ENSv2Domain, this is the on-chain owner address (the HCA account address if used). + """ owner: Account """ @@ -340,7 +351,9 @@ type ENSv1Domain implements Domain { """The namehash of this ENSv1 Domain.""" node: Node! - """The owner of this Domain.""" + """ + If this is an ENSv1Domain, this is the effective owner of the Domain. If this is an ENSv2Domain, this is the on-chain owner address (the HCA account address if used). + """ owner: Account """ @@ -440,7 +453,9 @@ type ENSv2Domain implements Domain { """ name: InterpretedName - """The owner of this Domain.""" + """ + If this is an ENSv1Domain, this is the effective owner of the Domain. If this is an ENSv2Domain, this is the on-chain owner address (the HCA account address if used). + """ owner: Account """ @@ -539,7 +554,9 @@ type ENSv2RegistryRegistration implements Registration { """The extra `referrer` data provided with a Registration, if exists.""" referrer: Hex - """The Registrant of a Registration, if exists.""" + """ + The Registrant of a Registration, if exists. For ENSv2 Registrations, the protocol-emitted registrant address (the HCA account address if used). + """ registrant: Account """The Registrar contract under which this Registration is managed.""" @@ -553,7 +570,9 @@ type ENSv2RegistryRegistration implements Registration { """A UnixTimestamp indicating when this Registration was created.""" start: BigInt! - """The Unregistrant of a Registration, if exists.""" + """ + The Unregistrant of a Registration, if exists. For ENSv2 Registrations, the protocol-emitted unregistrant address (the HCA account address if used). + """ unregistrant: Account } @@ -581,7 +600,9 @@ type ENSv2RegistryReservation implements Registration { """The extra `referrer` data provided with a Registration, if exists.""" referrer: Hex - """The Registrant of a Registration, if exists.""" + """ + The Registrant of a Registration, if exists. For ENSv2 Registrations, the protocol-emitted registrant address (the HCA account address if used). + """ registrant: Account """The Registrar contract under which this Registration is managed.""" @@ -595,7 +616,9 @@ type ENSv2RegistryReservation implements Registration { """A UnixTimestamp indicating when this Registration was created.""" start: BigInt! - """The Unregistrant of a Registration, if exists.""" + """ + The Unregistrant of a Registration, if exists. For ENSv2 Registrations, the protocol-emitted unregistrant address (the HCA account address if used). + """ unregistrant: Account } @@ -619,7 +642,7 @@ type Event { data: Hex! """ - Identifies the sender of the Transaction within which this Event was emitted. + Identifies the sender of the Transaction within which this Event was emitted (`tx.from`). Never HCA-aware — always the EOA/relayer that submitted the transaction. Use `Event.sender` for the HCA-aware actor. """ from: Address! @@ -629,6 +652,9 @@ type Event { """The index of this Event's log within the Block.""" logIndex: Int! + """The HCA account address if used, otherwise Transaction.from.""" + sender: Address! + """ The UnixTimestamp indicating the moment in which this Event was emitted. """ @@ -651,7 +677,9 @@ type Event { """Filter conditions for an events connection.""" input EventsWhereInput { - """Filter to events sent by this address.""" + """ + Filter to events whose `tx.from` matches. Not HCA-aware — use `sender` to filter by the HCA account address. + """ from: Address """ @@ -659,6 +687,11 @@ input EventsWhereInput { """ selector_in: [Hex!] + """ + Filter to events whose `sender` matches: the HCA account address if used, otherwise Transaction.from. + """ + sender: Address + """Filter to events at or after this UnixTimestamp.""" timestamp_gte: BigInt @@ -725,7 +758,9 @@ type NameWrapperRegistration implements Registration { """The extra `referrer` data provided with a Registration, if exists.""" referrer: Hex - """The Registrant of a Registration, if exists.""" + """ + The Registrant of a Registration, if exists. For ENSv2 Registrations, the protocol-emitted registrant address (the HCA account address if used). + """ registrant: Account """The Registrar contract under which this Registration is managed.""" @@ -739,7 +774,9 @@ type NameWrapperRegistration implements Registration { """A UnixTimestamp indicating when this Registration was created.""" start: BigInt! - """The Unregistrant of a Registration, if exists.""" + """ + The Unregistrant of a Registration, if exists. For ENSv2 Registrations, the protocol-emitted unregistrant address (the HCA account address if used). + """ unregistrant: Account } @@ -857,7 +894,9 @@ type PermissionsUser { """The Roles this User has been granted within this Resource.""" roles: BigInt! - """The User for whom these Roles are granted.""" + """ + The user/grantee address this Permission is granted to (the HCA account address if used). + """ user: Account! } @@ -979,7 +1018,9 @@ interface Registration { """The extra `referrer` data provided with a Registration, if exists.""" referrer: Hex - """The Registrant of a Registration, if exists.""" + """ + The Registrant of a Registration, if exists. For ENSv2 Registrations, the protocol-emitted registrant address (the HCA account address if used). + """ registrant: Account """The Registrar contract under which this Registration is managed.""" @@ -993,7 +1034,9 @@ interface Registration { """A UnixTimestamp indicating when this Registration was created.""" start: BigInt! - """The Unregistrant of a Registration, if exists.""" + """ + The Unregistrant of a Registration, if exists. For ENSv2 Registrations, the protocol-emitted unregistrant address (the HCA account address if used). + """ unregistrant: Account } @@ -1085,7 +1128,9 @@ type RegistryPermissionsUser { """The Roles that this Permission grants.""" roles: BigInt! - """The User for whom these Roles are granted.""" + """ + The user/grantee address this Permission is granted to (the HCA account address if used). + """ user: Account! } @@ -1172,7 +1217,9 @@ type ResolverPermissionsUser { """The Roles that this Permission grants.""" roles: BigInt! - """The User for whom these Roles are granted.""" + """ + The user/grantee address this Permission is granted to (the HCA account address if used). + """ user: Account! } @@ -1236,7 +1283,9 @@ type ThreeDNSRegistration implements Registration { """The extra `referrer` data provided with a Registration, if exists.""" referrer: Hex - """The Registrant of a Registration, if exists.""" + """ + The Registrant of a Registration, if exists. For ENSv2 Registrations, the protocol-emitted registrant address (the HCA account address if used). + """ registrant: Account """The Registrar contract under which this Registration is managed.""" @@ -1250,7 +1299,9 @@ type ThreeDNSRegistration implements Registration { """A UnixTimestamp indicating when this Registration was created.""" start: BigInt! - """The Unregistrant of a Registration, if exists.""" + """ + The Unregistrant of a Registration, if exists. For ENSv2 Registrations, the protocol-emitted unregistrant address (the HCA account address if used). + """ unregistrant: Account }