Skip to content
Merged
5 changes: 5 additions & 0 deletions .changeset/hca-aware-events.md
Original file line number Diff line number Diff line change
@@ -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`.
3 changes: 2 additions & 1 deletion apps/ensapi/src/lib/resolution/execute-operations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
type Hex,
type Name,
type RecordVersion,
toNormalizedAddress,
} from "enssdk";
import {
ContractFunctionExecutionError,
Expand Down Expand Up @@ -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)) };
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Comment thread
shrugs marked this conversation as resolved.

/**
Expand All @@ -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,
);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
});
});
Expand Down
5 changes: 3 additions & 2 deletions apps/ensapi/src/omnigraph-api/schema/account.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 } }),
Comment thread
shrugs marked this conversation as resolved.
}),
Comment thread
shrugs marked this conversation as resolved.

///////////////////////
Expand Down
3 changes: 2 additions & 1 deletion apps/ensapi/src/omnigraph-api/schema/domain.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
}),
Expand Down
32 changes: 27 additions & 5 deletions apps/ensapi/src/omnigraph-api/schema/event.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
////////////
Expand Down Expand Up @@ -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.",
Expand All @@ -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) => ({
Comment thread
shrugs marked this conversation as resolved.
selector_in: t.field({
type: ["Hex"],
Expand All @@ -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`.",
}),
}),
});
3 changes: 2 additions & 1 deletion apps/ensapi/src/omnigraph-api/schema/permissions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
6 changes: 4 additions & 2 deletions apps/ensapi/src/omnigraph-api/schema/registration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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,
Comment thread
shrugs marked this conversation as resolved.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ export const EventFragment = gql`
transactionHash
transactionIndex
from
sender
to
address
logIndex
Expand All @@ -38,6 +39,7 @@ export type EventResult = {
transactionHash: Hex;
transactionIndex: number;
from: NormalizedAddress;
sender: NormalizedAddress;
to: NormalizedAddress | null;
address: NormalizedAddress;
logIndex: number;
Expand Down
18 changes: 10 additions & 8 deletions apps/ensindexer/src/lib/ensv2/account-db-helpers.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { Address } from "enssdk";
import type { NormalizedAddress } from "enssdk";

import { interpretAddress } from "@ensnode/ensnode-sdk";

Expand All @@ -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<NormalizedAddress | null> {
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;
Comment thread
shrugs marked this conversation as resolved.
}
10 changes: 9 additions & 1 deletion apps/ensindexer/src/lib/ensv2/event-db-helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import {
type DomainId,
makePermissionsId,
makeResolverId,
type NormalizedAddress,
type PermissionsUserId,
} from "enssdk";
import type { Hash } from "viem";
Expand All @@ -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.`);
Expand All @@ -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,

Expand Down
34 changes: 18 additions & 16 deletions apps/ensindexer/src/plugins/ensv2/handlers/ensv2/ENSv2Registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Comment thread
shrugs marked this conversation as resolved.
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,
Expand Down Expand Up @@ -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);
},
);
Expand All @@ -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);
Expand All @@ -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);
},
);
Expand All @@ -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);
Expand Down Expand Up @@ -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);
},
);
Expand Down Expand Up @@ -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);
Expand All @@ -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);
}

Expand Down
Loading
Loading