Skip to content

Commit

Permalink
feat(plugin): index linea.eth (#13)
Browse files Browse the repository at this point in the history
Introduces the `linea.eth` plugin for Ponder indexer. The `linea.eth` plugin is very similar to the `eth` plugin. We can say it extends it (with minor naming alterations in some interfaces).
  • Loading branch information
tk-o authored Jan 9, 2025
1 parent 9d1b508 commit 7a9838f
Show file tree
Hide file tree
Showing 19 changed files with 2,814 additions and 179 deletions.
8 changes: 7 additions & 1 deletion .github/workflows/static-analysis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ on:
- main

jobs:
biome-ci:
static-analysis:
runs-on: ubuntu-latest

steps:
Expand All @@ -24,8 +24,14 @@ jobs:
- name: Install pnpm
run: npm install -g pnpm

- name: Audit dependencies
run: pnpm audit --audit-level=low

- name: Install dependencies
run: pnpm install

- name: Run Biome CI
run: pnpm biome ci

- name: Run TypeScript type checking
run: pnpm run typecheck
44 changes: 22 additions & 22 deletions ponder.config.ts
Original file line number Diff line number Diff line change
@@ -1,29 +1,29 @@
import { ACTIVE_PLUGIN } from "./src/lib/plugin-helpers";
import {
activate as activateEthBase,
config as ethBaseConfig,
ownedName as ethBaseOwnedName,
} from "./src/plugins/base.eth/ponder.config";
import {
activate as activateEth,
config as ethConfig,
ownedName as ethOwnedName,
} from "./src/plugins/eth/ponder.config";
import * as baseEthPlugin from "./src/plugins/base.eth/ponder.config";
import * as ethPlugin from "./src/plugins/eth/ponder.config";
import * as lineaEthPlugin from "./src/plugins/linea.eth/ponder.config";

type AllConfigs = typeof ethConfig & typeof ethBaseConfig;
const plugins = [baseEthPlugin, ethPlugin, lineaEthPlugin] as const;

// here we export only a single 'plugin's config, by type it as every config
// this makes all of the mapping types happy at typecheck-time, but only the relevant
// config is run at runtime
// this makes all of the mapping types happy at typecheck-time, but only the
// relevant config is run at runtime
export default ((): AllConfigs => {
switch (ACTIVE_PLUGIN) {
case ethOwnedName:
activateEth();
return ethConfig as AllConfigs;
case ethBaseOwnedName:
activateEthBase();
return ethBaseConfig as AllConfigs;
default:
throw new Error(`Unsupported ACTIVE_PLUGIN: ${ACTIVE_PLUGIN}`);
const pluginToActivate = plugins.find((p) => p.ownedName === ACTIVE_PLUGIN);

if (!pluginToActivate) {
throw new Error(`Unsupported ACTIVE_PLUGIN: ${ACTIVE_PLUGIN}`);
}

pluginToActivate.activate();

return pluginToActivate.config as AllConfigs;
})();

// Helper type to get the intersection of all config types
type IntersectionOf<T> = (T extends any ? (x: T) => void : never) extends (x: infer R) => void
? R
: never;
// The type of the exported default is the intersection of all plugin configs to
// each plugin can be correctly typechecked
type AllConfigs = IntersectionOf<(typeof plugins)[number]["config"]>;
298 changes: 151 additions & 147 deletions src/handlers/NameWrapper.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import { type Context, type Event, type EventNames } from "ponder:registry";
import { domains, wrappedDomains } from "ponder:schema";
import { checkPccBurned } from "@ensdomains/ensjs/utils";
import { type Address, type Hex, hexToBytes } from "viem";
import { type Address, type Hex, hexToBytes, namehash } from "viem";
import { bigintMax } from "../lib/helpers";
import { makeEventId } from "../lib/ids";
import { ETH_NODE, decodeDNSPacketBytes, tokenIdToLabel } from "../lib/subname-helpers";
import { decodeDNSPacketBytes, tokenIdToLabel } from "../lib/subname-helpers";
import { upsertAccount } from "../lib/upserts";

// if the wrappedDomain in question has pcc burned (?) and a higher (?) expiry date, update the domain's expiryDate
Expand Down Expand Up @@ -61,150 +61,154 @@ async function handleTransfer(

// TODO: log WrappedTransfer
}
export async function handleNameWrapped({
context,
event,
}: {
context: Context;
event: {
args: {
node: Hex;
owner: Hex;
fuses: number;
expiry: bigint;
name: Hex;
};
};
}) {
const { node, owner, fuses, expiry } = event.args;

await upsertAccount(context, owner);

// decode the name emitted by NameWrapper
const [label, name] = decodeDNSPacketBytes(hexToBytes(event.args.name));

// upsert the healed name iff valid
if (label) {
await context.db.update(domains, { id: node }).set({ labelName: label, name });
}

// update the WrappedDomain that was created in handleTransfer
await context.db.update(wrappedDomains, { id: node }).set({
name,
expiryDate: expiry,
fuses,
});

// materialize wrappedOwner relation
await context.db.update(domains, { id: node }).set({ wrappedOwnerId: owner });

// materialize domain expiryDate
await materializeDomainExpiryDate(context, node);

// TODO: log NameWrapped
}

export async function handleNameUnwrapped({
context,
event,
}: {
context: Context;
event: {
args: {
node: Hex;
owner: Hex;
};
};
}) {
const { node, owner } = event.args;

await upsertAccount(context, owner);

await context.db.update(domains, { id: node }).set((domain) => ({
// null expiry date if the domain is not a direct child of .eth
// https://github.com/ensdomains/ens-subgraph/blob/master/src/nameWrapper.ts#L123
...(domain.expiryDate && domain.parentId !== ETH_NODE && { expiryDate: null }),
ownerId: owner,
wrappedOwnerId: null,
}));

// delete the WrappedDomain
await context.db.delete(wrappedDomains, { id: node });

// TODO: log NameUnwrapped
}

export async function handleFusesSet({
context,
event,
}: {
context: Context;
event: {
args: {
node: Hex;
fuses: number;
};
export const makeNameWrapperHandlers = (ownedName: `${string}eth`) => {
const ownedSubnameNode = namehash(ownedName);

return {
async handleNameWrapped({
context,
event,
}: {
context: Context;
event: {
args: {
node: Hex;
owner: Hex;
fuses: number;
expiry: bigint;
name: Hex;
};
};
}) {
const { node, owner, fuses, expiry } = event.args;

await upsertAccount(context, owner);

// decode the name emitted by NameWrapper
const [label, name] = decodeDNSPacketBytes(hexToBytes(event.args.name));

// upsert the healed name iff valid
if (label) {
await context.db.update(domains, { id: node }).set({ labelName: label, name });
}

// update the WrappedDomain that was created in handleTransfer
await context.db.update(wrappedDomains, { id: node }).set({
name,
expiryDate: expiry,
fuses,
});

// materialize wrappedOwner relation
await context.db.update(domains, { id: node }).set({ wrappedOwnerId: owner });

// materialize domain expiryDate
await materializeDomainExpiryDate(context, node);

// TODO: log NameWrapped
},

async handleNameUnwrapped({
context,
event,
}: {
context: Context;
event: {
args: {
node: Hex;
owner: Hex;
};
};
}) {
const { node, owner } = event.args;

await upsertAccount(context, owner);

await context.db.update(domains, { id: node }).set((domain) => ({
// null expiry date if the domain is not a direct child of .eth
// https://github.com/ensdomains/ens-subgraph/blob/master/src/nameWrapper.ts#L123
...(domain.expiryDate && domain.parentId !== ownedSubnameNode && { expiryDate: null }),
ownerId: owner,
wrappedOwnerId: null,
}));

// delete the WrappedDomain
await context.db.delete(wrappedDomains, { id: node });

// TODO: log NameUnwrapped
},

async handleFusesSet({
context,
event,
}: {
context: Context;
event: {
args: {
node: Hex;
fuses: number;
};
};
}) {
const { node, fuses } = event.args;

// NOTE: subgraph does an implicit ignore if no WrappedDomain record.
// we will be more explicit and update logic if necessary
await context.db.update(wrappedDomains, { id: node }).set({ fuses });
},
async handleExpiryExtended({
context,
event,
}: {
context: Context;
event: {
args: {
node: Hex;
expiry: bigint;
};
};
}) {
const { node, expiry } = event.args;

// NOTE: subgraph does an implicit ignore if no WrappedDomain record.
// we will be more explicit and update logic if necessary
await context.db.update(wrappedDomains, { id: node }).set({ expiryDate: expiry });

// materialize the domain's expiryDate
await materializeDomainExpiryDate(context, node);

// TODO: log ExpiryExtended
},
async handleTransferSingle({
context,
event,
}: {
context: Context;
event: Event<EventNames> & {
args: {
id: bigint;
to: Hex;
};
};
}) {
await handleTransfer(context, event, makeEventId(event, 0), event.args.id, event.args.to);
},
async handleTransferBatch({
context,
event,
}: {
context: Context;
event: Event<EventNames> & {
args: {
ids: readonly bigint[];
to: Hex;
};
};
}) {
for (const [i, id] of event.args.ids.entries()) {
await handleTransfer(context, event, makeEventId(event, i), id, event.args.to);
}
},
};
}) {
const { node, fuses } = event.args;

// NOTE: subgraph does an implicit ignore if no WrappedDomain record.
// we will be more explicit and update logic if necessary
await context.db.update(wrappedDomains, { id: node }).set({ fuses });
}

export async function handleExpiryExtended({
context,
event,
}: {
context: Context;
event: {
args: {
node: Hex;
expiry: bigint;
};
};
}) {
const { node, expiry } = event.args;

// NOTE: subgraph does an implicit ignore if no WrappedDomain record.
// we will be more explicit and update logic if necessary
await context.db.update(wrappedDomains, { id: node }).set({ expiryDate: expiry });

// materialize the domain's expiryDate
await materializeDomainExpiryDate(context, node);

// TODO: log ExpiryExtended
}

export async function handleTransferSingle({
context,
event,
}: {
context: Context;
event: Event<EventNames> & {
args: {
id: bigint;
to: Hex;
};
};
}) {
return await handleTransfer(context, event, makeEventId(event, 0), event.args.id, event.args.to);
}

export async function handleTransferBatch({
context,
event,
}: {
context: Context;
event: Event<EventNames> & {
args: {
ids: readonly bigint[];
to: Hex;
};
};
}) {
for (const [i, id] of event.args.ids.entries()) {
await handleTransfer(context, event, makeEventId(event, i), id, event.args.to);
}
}
};
2 changes: 1 addition & 1 deletion src/handlers/Registrar.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ const GRACE_PERIOD_SECONDS = 7776000n; // 90 days in seconds
/**
* A factory function that returns Ponder indexing handlers for a specified subname.
*/
export const makeRegistryHandlers = (ownedName: `${string}eth`) => {
export const makeRegistrarHandlers = (ownedName: `${string}eth`) => {
const ownedSubnameNode = namehash(ownedName);

async function setNamePreimage(context: Context, name: string, label: Hex, cost: bigint) {
Expand Down
Loading

0 comments on commit 7a9838f

Please sign in to comment.