From be30eefe8c1c540e2d1342350256013fe68c8723 Mon Sep 17 00:00:00 2001 From: sevenzing Date: Fri, 24 Apr 2026 12:39:19 +0400 Subject: [PATCH 01/18] add initial devnet seeding --- .../resolve-primary-name.integration.test.ts | 15 ++- .../resolve-primary-names.integration.test.ts | 14 +-- .../resolve-records.integration.test.ts | 16 ++-- .../schema/account.integration.test.ts | 32 +++---- packages/datasources/src/lib/chains.ts | 2 +- packages/ensnode-sdk/src/internal.ts | 3 +- .../src/omnigraph-api/example-queries.ts | 14 +-- .../ensnode-sdk/src/shared/devnet-accounts.ts | 14 --- .../ensnode-sdk/src/shared/devnet/accounts.ts | 26 +++++ .../src/shared/devnet/addresses.ts | 96 +++++++++++++++++++ packages/integration-test-env/package.json | 3 +- .../integration-test-env/src/orchestrator.ts | 12 ++- packages/integration-test-env/src/seed/abi.ts | 6 ++ .../integration-test-env/src/seed/index.ts | 44 +++++++++ .../src/seed/primary-names.ts | 18 ++++ pnpm-lock.yaml | 3 + 16 files changed, 252 insertions(+), 66 deletions(-) delete mode 100644 packages/ensnode-sdk/src/shared/devnet-accounts.ts create mode 100644 packages/ensnode-sdk/src/shared/devnet/accounts.ts create mode 100644 packages/ensnode-sdk/src/shared/devnet/addresses.ts create mode 100644 packages/integration-test-env/src/seed/abi.ts create mode 100644 packages/integration-test-env/src/seed/index.ts create mode 100644 packages/integration-test-env/src/seed/primary-names.ts diff --git a/apps/ensapi/src/handlers/api/resolution/resolve-primary-name.integration.test.ts b/apps/ensapi/src/handlers/api/resolution/resolve-primary-name.integration.test.ts index 5b4ff39ec3..d72606853d 100644 --- a/apps/ensapi/src/handlers/api/resolution/resolve-primary-name.integration.test.ts +++ b/apps/ensapi/src/handlers/api/resolution/resolve-primary-name.integration.test.ts @@ -6,25 +6,24 @@ import { describe, expect, it } from "vitest"; -import { DEVNET_OWNER, DEVNET_USER } from "@ensnode/ensnode-sdk/internal"; +import { DEVNET_ACCOUNTS } from "@ensnode/ensnode-sdk/internal"; const BASE_URL = process.env.ENSNODE_URL!; describe("GET /api/resolve/primary-name/:address/:chainId", () => { it.each([ { - description: - "resolves primary name for owner address on chain 1 (no primary name set in devnet)", - address: DEVNET_OWNER, + description: "resolves primary name for owner address on chain 1", + address: DEVNET_ACCOUNTS.owner, chainId: "1", query: "", expectedStatus: 200, - expectedBody: { name: null, accelerationRequested: false, accelerationAttempted: false }, + expectedBody: { name: "test.eth", accelerationRequested: false, accelerationAttempted: false }, }, { description: "resolves primary name for user address on chain 1 (no primary name set in devnet)", - address: DEVNET_USER, + address: DEVNET_ACCOUNTS.user, chainId: "1", query: "", expectedStatus: 200, @@ -32,7 +31,7 @@ describe("GET /api/resolve/primary-name/:address/:chainId", () => { }, { description: "owner address with accelerate=true returns accelerationRequested: true", - address: DEVNET_OWNER, + address: DEVNET_ACCOUNTS.owner, chainId: "1", query: "accelerate=true", expectedStatus: 200, @@ -58,7 +57,7 @@ describe("GET /api/resolve/primary-name/:address/:chainId", () => { }, { description: "returns 400 for non-numeric chainId", - address: DEVNET_OWNER, + address: DEVNET_ACCOUNTS.owner, chainId: "notachainid", query: "", expectedStatus: 400, diff --git a/apps/ensapi/src/handlers/api/resolution/resolve-primary-names.integration.test.ts b/apps/ensapi/src/handlers/api/resolution/resolve-primary-names.integration.test.ts index 0e54e01691..06cf4d7af6 100644 --- a/apps/ensapi/src/handlers/api/resolution/resolve-primary-names.integration.test.ts +++ b/apps/ensapi/src/handlers/api/resolution/resolve-primary-names.integration.test.ts @@ -6,7 +6,7 @@ import { describe, expect, it } from "vitest"; -import { DEVNET_OWNER } from "@ensnode/ensnode-sdk/internal"; +import { DEVNET_ACCOUNTS } from "@ensnode/ensnode-sdk/internal"; const BASE_URL = process.env.ENSNODE_URL!; @@ -15,22 +15,22 @@ describe("GET /api/resolve/primary-names/:address", () => { { description: "resolves primary names for owner address on chain 1 (no primary name set in devnet)", - address: DEVNET_OWNER, + address: DEVNET_ACCOUNTS.owner, query: "chainIds=1", expectedStatus: 200, expectedBody: { - names: { "1": null }, + names: { "1": "test.eth" }, accelerationRequested: false, accelerationAttempted: false, }, }, { description: "resolves all primary names", - address: DEVNET_OWNER, + address: DEVNET_ACCOUNTS.owner, query: "", expectedStatus: 200, expectedBody: { - names: { "1": null }, + names: { "1": "test.eth" }, accelerationRequested: false, accelerationAttempted: false, }, @@ -54,7 +54,7 @@ describe("GET /api/resolve/primary-names/:address", () => { }, { description: "returns 400 when chainIds contains the default chain id (0)", - address: DEVNET_OWNER, + address: DEVNET_ACCOUNTS.owner, query: "chainIds=0", expectedStatus: 400, expectedBody: { @@ -76,7 +76,7 @@ describe("GET /api/resolve/primary-names/:address", () => { }, { description: "returns 400 when chainIds contains duplicate chain ids", - address: DEVNET_OWNER, + address: DEVNET_ACCOUNTS.owner, query: "chainIds=1,1", expectedStatus: 400, expectedBody: { diff --git a/apps/ensapi/src/handlers/api/resolution/resolve-records.integration.test.ts b/apps/ensapi/src/handlers/api/resolution/resolve-records.integration.test.ts index 329696d032..fe2e5ecdc1 100644 --- a/apps/ensapi/src/handlers/api/resolution/resolve-records.integration.test.ts +++ b/apps/ensapi/src/handlers/api/resolution/resolve-records.integration.test.ts @@ -6,7 +6,7 @@ import { describe, expect, it } from "vitest"; -import { DEVNET_OWNER } from "@ensnode/ensnode-sdk/internal"; +import { DEVNET_ACCOUNTS } from "@ensnode/ensnode-sdk/internal"; const BASE_URL = process.env.ENSNODE_URL!; @@ -18,7 +18,7 @@ describe("GET /api/resolve/records/:name", () => { query: "addresses=60", expectedStatus: 200, expectedBody: { - records: { addresses: { 60: DEVNET_OWNER } }, + records: { addresses: { 60: DEVNET_ACCOUNTS.owner } }, accelerationRequested: false, accelerationAttempted: false, }, @@ -30,7 +30,7 @@ describe("GET /api/resolve/records/:name", () => { query: "addresses=60", expectedStatus: 200, expectedBody: { - records: { addresses: { 60: DEVNET_OWNER } }, + records: { addresses: { 60: DEVNET_ACCOUNTS.owner } }, accelerationRequested: false, accelerationAttempted: false, }, @@ -65,7 +65,7 @@ describe("GET /api/resolve/records/:name", () => { expectedStatus: 200, expectedBody: { records: { - addresses: { 60: DEVNET_OWNER }, + addresses: { 60: DEVNET_ACCOUNTS.owner }, texts: { description: "example.eth" }, }, accelerationRequested: false, @@ -90,7 +90,7 @@ describe("GET /api/resolve/records/:name", () => { query: "addresses=60", expectedStatus: 200, expectedBody: { - records: { addresses: { 60: DEVNET_OWNER } }, + records: { addresses: { 60: DEVNET_ACCOUNTS.owner } }, accelerationRequested: false, accelerationAttempted: false, }, @@ -112,7 +112,7 @@ describe("GET /api/resolve/records/:name", () => { query: "addresses=60", expectedStatus: 200, expectedBody: { - records: { addresses: { 60: DEVNET_OWNER } }, + records: { addresses: { 60: DEVNET_ACCOUNTS.owner } }, accelerationRequested: false, accelerationAttempted: false, }, @@ -124,7 +124,7 @@ describe("GET /api/resolve/records/:name", () => { query: "addresses=60", expectedStatus: 200, expectedBody: { - records: { addresses: { 60: DEVNET_OWNER } }, + records: { addresses: { 60: DEVNET_ACCOUNTS.owner } }, accelerationRequested: false, accelerationAttempted: false, }, @@ -135,7 +135,7 @@ describe("GET /api/resolve/records/:name", () => { query: "addresses=60&accelerate=true", expectedStatus: 200, expectedBody: { - records: { addresses: { 60: DEVNET_OWNER } }, + records: { addresses: { 60: DEVNET_ACCOUNTS.owner } }, accelerationRequested: true, accelerationAttempted: false, }, 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 b3f14f8c94..8ae5d71b7f 100644 --- a/apps/ensapi/src/omnigraph-api/schema/account.integration.test.ts +++ b/apps/ensapi/src/omnigraph-api/schema/account.integration.test.ts @@ -1,7 +1,7 @@ import type { InterpretedName } from "enssdk"; import { beforeAll, describe, expect, it } from "vitest"; -import { DEVNET_DEPLOYER, DEVNET_OWNER, DEVNET_USER } from "@ensnode/ensnode-sdk/internal"; +import { DEVNET_ACCOUNTS } from "@ensnode/ensnode-sdk/internal"; import { AccountDomainsPaginated, @@ -36,7 +36,7 @@ describe("Account.domains", () => { `; it("returns domains owned by the devnet owner", async () => { - const result = await request(AccountDomains, { address: DEVNET_OWNER }); + const result = await request(AccountDomains, { address: DEVNET_ACCOUNTS.owner }); const domains = flattenConnection(result.account.domains); const names = domains.map((d) => d.name); @@ -62,7 +62,7 @@ describe("Account.domains", () => { it("returns domains owned by the new owner", async () => { const result = await request(AccountDomains, { - address: DEVNET_USER, + address: DEVNET_ACCOUNTS.user, }); const domains = flattenConnection(result.account.domains); const names = domains.map((d) => d.name); @@ -75,7 +75,7 @@ describe("Account.domains pagination", () => { testDomainPagination(async (variables) => { const result = await request<{ account: { domains: PaginatedGraphQLConnection }; - }>(AccountDomainsPaginated, { address: DEVNET_OWNER, ...variables }); + }>(AccountDomainsPaginated, { address: DEVNET_ACCOUNTS.owner, ...variables }); return result.account.domains; }); }); @@ -92,14 +92,14 @@ describe("Account.events", () => { it("returns events for the devnet deployer", async () => { const result = await request(AccountEvents, { - address: DEVNET_DEPLOYER, + address: DEVNET_ACCOUNTS.deployer, }); const events = flattenConnection(result.account.events); expect(events.length).toBeGreaterThan(0); for (const event of events) { - expect(event.from).toBe(DEVNET_DEPLOYER); + expect(event.from).toBe(DEVNET_ACCOUNTS.deployer); } }); }); @@ -108,7 +108,7 @@ describe("Account.events pagination", () => { testEventPagination(async (variables) => { const result = await request<{ account: { events: PaginatedGraphQLConnection }; - }>(AccountEventsPaginated, { address: DEVNET_DEPLOYER, ...variables }); + }>(AccountEventsPaginated, { address: DEVNET_ACCOUNTS.deployer, ...variables }); return result.account.events; }); }); @@ -127,7 +127,7 @@ describe("Account.events filtering (AccountEventsWhereInput)", () => { beforeAll(async () => { const result = await request(AccountEventsFiltered, { - address: DEVNET_DEPLOYER, + address: DEVNET_ACCOUNTS.deployer, first: 1000, }); // events are returned in ascending order, so first/last access yields min/max values @@ -139,7 +139,7 @@ describe("Account.events filtering (AccountEventsWhereInput)", () => { const targetSelector = allEvents[0].topics[0]; const result = await request(AccountEventsFiltered, { - address: DEVNET_DEPLOYER, + address: DEVNET_ACCOUNTS.deployer, where: { selector_in: [targetSelector] }, }); const events = flattenConnection(result.account.events); @@ -152,7 +152,7 @@ describe("Account.events filtering (AccountEventsWhereInput)", () => { it("filters by selector_in with unknown topic returns no results", async () => { const result = await request(AccountEventsFiltered, { - address: DEVNET_DEPLOYER, + address: DEVNET_ACCOUNTS.deployer, where: { selector_in: ["0x0000000000000000000000000000000000000000000000000000000000000001"], }, @@ -163,7 +163,7 @@ describe("Account.events filtering (AccountEventsWhereInput)", () => { it("filters by empty selector_in returns no results", async () => { const result = await request(AccountEventsFiltered, { - address: DEVNET_DEPLOYER, + address: DEVNET_ACCOUNTS.deployer, where: { selector_in: [] }, }); const events = flattenConnection(result.account.events); @@ -174,7 +174,7 @@ describe("Account.events filtering (AccountEventsWhereInput)", () => { const midTimestamp = allEvents[Math.floor(allEvents.length / 2)].timestamp; const result = await request(AccountEventsFiltered, { - address: DEVNET_DEPLOYER, + address: DEVNET_ACCOUNTS.deployer, where: { timestamp_gte: midTimestamp }, }); const events = flattenConnection(result.account.events); @@ -190,7 +190,7 @@ describe("Account.events filtering (AccountEventsWhereInput)", () => { const midTimestamp = allEvents[Math.floor(allEvents.length / 2)].timestamp; const result = await request(AccountEventsFiltered, { - address: DEVNET_DEPLOYER, + address: DEVNET_ACCOUNTS.deployer, where: { timestamp_lte: midTimestamp }, }); const events = flattenConnection(result.account.events); @@ -207,7 +207,7 @@ describe("Account.events filtering (AccountEventsWhereInput)", () => { const maxTs = allEvents[allEvents.length - 1].timestamp; const result = await request(AccountEventsFiltered, { - address: DEVNET_DEPLOYER, + address: DEVNET_ACCOUNTS.deployer, where: { timestamp_gte: minTs, timestamp_lte: maxTs }, first: 1000, }); @@ -224,7 +224,7 @@ describe("Account.events filtering (AccountEventsWhereInput)", () => { const midTimestamp = seedEvent.timestamp; const result = await request(AccountEventsFiltered, { - address: DEVNET_DEPLOYER, + address: DEVNET_ACCOUNTS.deployer, where: { selector_in: [targetSelector], timestamp_gte: midTimestamp }, }); const events = flattenConnection(result.account.events); @@ -241,7 +241,7 @@ describe("Account.events filtering (AccountEventsWhereInput)", () => { const maxTimestamp = BigInt(allEvents[allEvents.length - 1].timestamp); const result = await request(AccountEventsFiltered, { - address: DEVNET_DEPLOYER, + address: DEVNET_ACCOUNTS.deployer, where: { timestamp_gte: (maxTimestamp + 1n).toString() }, }); const events = flattenConnection(result.account.events); diff --git a/packages/datasources/src/lib/chains.ts b/packages/datasources/src/lib/chains.ts index 851dfff825..736c1c4485 100644 --- a/packages/datasources/src/lib/chains.ts +++ b/packages/datasources/src/lib/chains.ts @@ -4,7 +4,7 @@ import { type Chain, localhost } from "viem/chains"; * The Default Chain Id for Devnet * @see https://github.com/ensdomains/contracts-v2/blob/762de44d60b2588b2e92a6d29df941c4de821ae6/contracts/script/setup.ts#L40 */ -const DEVNET_DEFAULT_CHAIN_ID = 0xeeeeed; +const DEVNET_DEFAULT_CHAIN_ID = 1; export const ensTestEnvChain = { ...localhost, diff --git a/packages/ensnode-sdk/src/internal.ts b/packages/ensnode-sdk/src/internal.ts index 01743d2375..97786b03b1 100644 --- a/packages/ensnode-sdk/src/internal.ts +++ b/packages/ensnode-sdk/src/internal.ts @@ -39,7 +39,8 @@ export * from "./shared/config/zod-schemas"; export * from "./shared/config-templates"; export * from "./shared/datasources-with-ensv2-contracts"; export * from "./shared/datasources-with-resolvers"; -export * from "./shared/devnet-accounts"; +export * from "./shared/devnet/accounts"; +export * from "./shared/devnet/addresses"; export * from "./shared/interpretation/interpret-address"; export * from "./shared/interpretation/interpret-record-values"; export * from "./shared/interpretation/interpret-resolver-values"; diff --git a/packages/ensnode-sdk/src/omnigraph-api/example-queries.ts b/packages/ensnode-sdk/src/omnigraph-api/example-queries.ts index 6f49dc01d6..9051f63d76 100644 --- a/packages/ensnode-sdk/src/omnigraph-api/example-queries.ts +++ b/packages/ensnode-sdk/src/omnigraph-api/example-queries.ts @@ -3,6 +3,7 @@ import { asInterpretedName, toNormalizedAddress } from "enssdk"; import { DatasourceNames, ENSNamespaceIds } from "@ensnode/datasources"; import { maybeGetDatasourceContract } from "../shared/datasource-contract"; +import { DEVNET_ACCOUNTS } from "../shared/devnet/accounts"; import type { NamespaceSpecificValue } from "../shared/namespace-specific-value"; const SEPOLIA_V2_V2_ETH_REGISTRY = maybeGetDatasourceContract( @@ -29,11 +30,6 @@ const ENS_TEST_ENV_V2_ETH_REGISTRAR = maybeGetDatasourceContract( "ETHRegistrar", ); -// these addresses are from the devnet accounts output -const DEVNET_DEPLOYER = toNormalizedAddress("0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266"); -const DEVNET_OWNER = toNormalizedAddress("0x70997970C51812dc3A010C7d01b50e0d17dc79C8"); -// biome-ignore lint/correctness/noUnusedVariables: keeping it around for the future -const DEVNET_USER = toNormalizedAddress("0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC"); const VITALIK_ADDRESS = toNormalizedAddress("0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045"); @@ -186,7 +182,7 @@ query AccountDomains( }`, variables: { default: { address: VITALIK_ADDRESS }, - [ENSNamespaceIds.EnsTestEnv]: { address: DEVNET_OWNER }, + [ENSNamespaceIds.EnsTestEnv]: { address: DEVNET_ACCOUNTS.owner }, }, }, @@ -204,7 +200,7 @@ query AccountEvents( }`, variables: { default: { address: VITALIK_ADDRESS }, - [ENSNamespaceIds.EnsTestEnv]: { address: DEVNET_DEPLOYER }, + [ENSNamespaceIds.EnsTestEnv]: { address: DEVNET_ACCOUNTS.deployer }, }, }, @@ -287,7 +283,7 @@ query PermissionsByUser($address: Address!) { } }`, variables: { - default: { address: DEVNET_DEPLOYER }, + default: { address: DEVNET_ACCOUNTS.deployer }, // TODO: figure out a good sepolia-v2 user address // [ENSNamespaceIds.SepoliaV2]: { address: "" }, }, @@ -314,7 +310,7 @@ query AccountResolverPermissions($address: Address!) { } }`, variables: { - default: { address: DEVNET_DEPLOYER }, + default: { address: DEVNET_ACCOUNTS.deployer }, // TODO: figure out a good sepolia-v2 user address // [ENSNamespaceIds.SepoliaV2]: { address: "" }, }, diff --git a/packages/ensnode-sdk/src/shared/devnet-accounts.ts b/packages/ensnode-sdk/src/shared/devnet-accounts.ts deleted file mode 100644 index ab9e733b4b..0000000000 --- a/packages/ensnode-sdk/src/shared/devnet-accounts.ts +++ /dev/null @@ -1,14 +0,0 @@ -/** - * Named accounts from the ens-test-env devnet. - * They are NOT real Ethereum Mainnet or testnet addresses. - * You can use `docker compose up devnet` to see actual data in devnet - * - * @see https://github.com/ensdomains/ens-test-env - */ - -import { toNormalizedAddress } from "enssdk"; - -export const DEVNET_DEPLOYER = toNormalizedAddress("0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266"); -export const DEVNET_OWNER = toNormalizedAddress("0x70997970c51812dc3a010c7d01b50e0d17dc79c8"); -export const DEVNET_USER = toNormalizedAddress("0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC"); -export const DEVNET_USER2 = toNormalizedAddress("0x90F79bf6EB2c4f870365E785982E1f101E93b906"); diff --git a/packages/ensnode-sdk/src/shared/devnet/accounts.ts b/packages/ensnode-sdk/src/shared/devnet/accounts.ts new file mode 100644 index 0000000000..90573430f0 --- /dev/null +++ b/packages/ensnode-sdk/src/shared/devnet/accounts.ts @@ -0,0 +1,26 @@ +/** + * Named accounts from the ens-test-env devnet. + * Derived from the standard Hardhat mnemonic at account indices 0–3. + * They are NOT real Ethereum Mainnet or testnet addresses. + * + * @see https://github.com/ensdomains/ens-test-env + * @see https://github.com/ensdomains/contracts-v2/blob/42f2016e7ba87eb3854afe51ad55186a16b32a74/contracts/script/setup.ts#L55 + */ + +import { toNormalizedAddress } from "enssdk"; + +/** + * Standard Hardhat/Anvil mnemonic used by the ens-test-env devnet. + */ +export const DEVNET_MNEMONIC = "test test test test test test test test test test test junk"; + +export const DEVNET_ACCOUNTS = { + /** Account index 0 — has REGISTRAR role on ETHRegistry */ + deployer: toNormalizedAddress("0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266"), + /** Account index 1 — owns test.eth */ + owner: toNormalizedAddress("0x70997970c51812dc3a010c7d01b50e0d17dc79c8"), + /** Account index 2 */ + user: toNormalizedAddress("0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC"), + /** Account index 3 */ + user2: toNormalizedAddress("0x90F79bf6EB2c4f870365E785982E1f101E93b906"), +} as const; diff --git a/packages/ensnode-sdk/src/shared/devnet/addresses.ts b/packages/ensnode-sdk/src/shared/devnet/addresses.ts new file mode 100644 index 0000000000..eb6b5c7282 --- /dev/null +++ b/packages/ensnode-sdk/src/shared/devnet/addresses.ts @@ -0,0 +1,96 @@ +/** + * Deterministic contract addresses from the ens-test-env devnet. + * These addresses are produced by the Hardhat/Anvil deploy scripts in contracts-v2 + * and are stable as long as the mnemonic and deploy order remain unchanged. + * + * Source: `pnpm devnet` output table + * @see https://github.com/ensdomains/contracts-v2 + */ + +export const DEVNET_CONTRACTS = { + // -- DNS -- + dnssecGatewayProvider: "0x5FbDB2315678afecb367f032d93F642f64180aa3", + dnsTxtResolver: "0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512", + dnsAliasResolver: "0x322813Fd9A801c5507c9de605d63CEA4f2CE6c44", + dnsTldResolver: "0x998abeb3E57409262aE5b751f60747921B33613E", + offchainDnsResolver: "0x851356ae760d987E095750cCeb3bC6014560891C", + simplePublicSuffixList: "0xf5059a5D33d5853360D16C683c16e67980206f36", + dnsRegistrar: "0x202CCe504e04bEd6fC0521238dDf04Bc9E8E15aB", + extendedDnsResolver: "0x4631BCAbD6dF18D94796344963cB60d44a4136b6", + + // -- Registries -- + legacyEnsRegistry: "0x9fE46736679d2D9a65F0992F2272dE9f3c7fa6e0", + ensRegistry: "0xCf7Ed3AccA5a467e9e704C703E8D87F634fB0Fc9", + rootRegistry: "0x2279B7A0a67DB372996a5FaB50D91eAA73d2eBe6", + ethRegistry: "0x8f86403A4DE0BB5791fa46B8e795C547942fE4Cf", + reverseRegistry: "0xCD8a1C3ba11CF5ECfa6267617243239504a98d90", + + // -- Registrars & Controllers -- + baseRegistrar: "0x8A791620dd6260079BF849Dc5567aDC3F2FdC318", + ethRegistrar: "0x4C4a2f8c81640e47606d3fd77B353E87Ba015584", + legacyEthRegistrarController: "0xfbC22278A96299D91d41C453234d97b4F5Eb9B2d", + wrappedEthRegistrarController: "0x253553366Da8546fC250F225fe3d25d0C782303b", + ethRegistrarController: "0x1c85638e118b37167e9298c2268758e058DdfDA0", + batchRegistrar: "0xD8a5a9b31c3C0232E196d518E89Fd8bF83AcAd43", + + // -- Reverse Resolution -- + ethReverseRegistrar: "0x59b670e9fA9D0A427751Af201D676719a970857b", + defaultReverseRegistrar: "0x4c5859f0F772848b2D91F1D83E2Fe57935348029", + defaultReverseResolver: "0x5f3f1dBD7B74C6B46e8c44f98792A1dAf8d69154", + ethReverseResolver: "0x7bc06c482DEAd17c0e297aFbC32f6e63d3846650", + reverseRegistrar: "0x162A433068F51e18b7d13932F27e66a3f99E6890", + l2ReverseRegistrar: "0x49fd2BE640DB2910c2fAb69bB8531Ab6E76127ff", + + // -- Resolvers -- + ensv1Resolver: "0x5FC8d32690cc91D4c39d9d3abcBD16989F875707", + ensv2Resolver: "0xc6e7DF5E7b4f2A278906862b61205850344D4e7d", + ownedResolver: "0x68B1D87F95878fE05B998F19b66F4baba5De1aed", + legacyPublicResolver: "0x86A2EE8FAf9A840F7a2c64CA3d51209F9A02081D", + publicResolver: "0xA4899D35897033b927acFCf422bc745916139776", + permissionedResolver: "0x809d550fca64d94Bd9F66E60752A544199cfAC3D", + universalResolver: "0x5067457698Fd6Fa1C6964e416b3f42713513B3dD", + universalResolverV2: "0x8198f5d8F8CfFE8f9C413d98a0A55aEB8ab9FbB7", + upgradableUniversalResolverProxy: "0x0355B7B8cb128fA5692729Ab3AAa199C1753f726", + + // -- L2 Reverse Resolvers -- + arbitrumReverseResolver: "0xf953b3A269d80e3eB0F2947630Da976B896A8C5b", + baseReverseResolver: "0xAA292E8611aDF267e563f334Ee42320aC96D0463", + lineaReverseResolver: "0x5c74c94173F05dA1720953407cbb920F3DF9f887", + optimismReverseResolver: "0x720472c8ce72c2A2D711333e064ABD3E6BbEAdd3", + scrollReverseResolver: "0xe8D2A1E88c91DCd5433208d4152Cc4F399a7e91d", + + // -- Infrastructure -- + batchGatewayProvider: "0xDc64a140Aa3E981100a9becA4E685f962f0cF6C9", + hcaFactory: "0x0165878A594ca255338adfa4d48449f69242Eb8F", + simpleRegistryMetadata: "0xa513E6E4b8f2a923D98304ec87F64353C4D5C853", + root: "0x610178dA211FEF7D417bC0e6FeD39F05609AD788", + rootSecurityController: "0xB7f8BC63BbcaD18155201308C8f3540b07f84F5e", + registrarSecurityController: "0x0B306BF915C4d645ff596e518fAf3F9669b97016", + verifiableFactory: "0x4ed7c70F96B99c776995fB64377f0d4aB3B0e1C1", + nameWrapper: "0x5081a39b8A5f0E35a8D959395a630b68B74Dd30f", + unlockedMigrationController: "0xdbC43Ba45381e02825b14322cDdd15eC4B3164E6", + wrapperRegistry: "0x2E2Ed0Cfd3AD2f1d34481277b3204d807Ca2F8c2", + lockedMigrationController: "0x51A1ceB83B83F1985a81C295d1fF28Afef186E02", + userRegistry: "0x7969c5eD335650692Bc04293B07F5BF2e7A673C0", + staticMetadataService: "0xB0D4afd8879eD9F52b28595d31B441D079B2Ca07", + multicall3: "0xcA11bde05977b3631167028862bE2a173976CA11", + migrationHelper: "0x18E317A7D70d8fBf8e6E893616b52390EbBdb629", + + // -- DNSSEC Algorithms & Digests -- + rsasha1Algorithm: "0xa85233C63b9Ee964Add6F2cffe00Fd84eb32338f", + rsasha256Algorithm: "0x4A679253410272dd5232B3Ff7cF5dbB88f295319", + p256sha256Algorithm: "0x7a2088a1bFc9d81c55368AE168C2C02570cB814F", + sha1Digest: "0x09635F643e140090A9A8Dcd712eD6285858ceBef", + sha256Digest: "0xc5a5C42992dECbae36851359345FE25997F5C42d", + dnssecImpl: "0x67d269191c92Caf3cD7723F116c85e6E9bf55933", + + // -- Pricing -- + standardRentPriceOracle: "0x1429859428C0aBc9C2C47C8Ee9FBaf82cFA0F20f", + staticBulkRenewal: "0x4C2F7092C2aE51D986bEFEe378e50BD4dB99C901", + dummyOracle: "0xD84379CEae14AA33C123Af12424A37803F885889", + exponentialPremiumPriceOracle: "0x2B0d36FACD61B71CC05ab8F3D2355ec3631C0dd5", + + // -- Mock Tokens -- + mockUsdc: "0xFD471836031dc5108809D173A067e8486B9047A3", + mockDai: "0xcbEAF3BDe82155F56486Fb5a1072cb8baAf547cc", +} as const; diff --git a/packages/integration-test-env/package.json b/packages/integration-test-env/package.json index 4b5e221f48..7a3e75494f 100644 --- a/packages/integration-test-env/package.json +++ b/packages/integration-test-env/package.json @@ -16,6 +16,7 @@ "@ensnode/ensnode-sdk": "workspace:*", "execa": "^9.6.1", "testcontainers": "^11.12.0", - "tsx": "^4.7.1" + "tsx": "^4.7.1", + "viem": "catalog:" } } diff --git a/packages/integration-test-env/src/orchestrator.ts b/packages/integration-test-env/src/orchestrator.ts index 99df9f5f46..c923f2ecf9 100644 --- a/packages/integration-test-env/src/orchestrator.ts +++ b/packages/integration-test-env/src/orchestrator.ts @@ -41,6 +41,8 @@ import { import { ENSNamespaceIds } from "@ensnode/datasources"; import { OmnichainIndexingStatusIds } from "@ensnode/ensnode-sdk"; +import { seedDevnet } from "./seed/index"; + const MONOREPO_ROOT = resolve(import.meta.dirname, "../../.."); const ENSRAINBOW_DIR = resolve(MONOREPO_ROOT, "apps/ensrainbow"); const ENSINDEXER_DIR = resolve(MONOREPO_ROOT, "apps/ensindexer"); @@ -248,7 +250,15 @@ async function main() { const postgresPort = postgresContainer.getMappedPort(5432); const ENSDB_URL = `postgresql://postgres:password@localhost:${postgresPort}/postgres`; log(`Postgres is ready (port ${postgresPort})`); - log("Devnet is ready"); + const devnetContainer = composeEnvironment.getContainer("devnet"); + const devnetPort = devnetContainer.getMappedPort(8545); + const devnetRpcUrl = `http://localhost:${devnetPort}`; + log(`Devnet is ready (port ${devnetPort})`); + + // Phase 1.5: Seed devnet with test data (before indexing starts) + log("Seeding devnet..."); + await seedDevnet(devnetRpcUrl); + log("Devnet seeded"); // Phase 2: Download ENSRainbow database and start from source const DB_SCHEMA_VERSION = "3"; diff --git a/packages/integration-test-env/src/seed/abi.ts b/packages/integration-test-env/src/seed/abi.ts new file mode 100644 index 0000000000..bc251ab7d3 --- /dev/null +++ b/packages/integration-test-env/src/seed/abi.ts @@ -0,0 +1,6 @@ +import { parseAbi } from "viem"; + +export const ethReverseRegistrarAbi = parseAbi([ + // https://github.com/ensdomains/contracts-v2/blob/42f2016e7ba87eb3854afe51ad55186a16b32a74/contracts/src/reverse-registrar/L2ReverseRegistrar.sol#L117-L119 + "function setName(string name) returns (bytes32)", +]); diff --git a/packages/integration-test-env/src/seed/index.ts b/packages/integration-test-env/src/seed/index.ts new file mode 100644 index 0000000000..f1aab1f3f5 --- /dev/null +++ b/packages/integration-test-env/src/seed/index.ts @@ -0,0 +1,44 @@ +import { + type Account, + type Chain, + createWalletClient, + http, + type Transport, + type WalletClient, +} from "viem"; +import { mnemonicToAccount } from "viem/accounts"; + +import { ensTestEnvChain } from "@ensnode/datasources"; +import { DEVNET_MNEMONIC } from "@ensnode/ensnode-sdk/internal"; + +import { seedPrimaryNameRecords } from "./primary-names"; + +export type DevnetWalletClient = WalletClient; + +export type DevnetWalletClients = { + deployer: DevnetWalletClient; // index 0 — has REGISTRAR role on ETHRegistry + owner: DevnetWalletClient; // index 1 — DEVNET_OWNER, owns test.eth + user: DevnetWalletClient; // index 2 — DEVNET_USER + user2: DevnetWalletClient; // index 3 — DEVNET_USER2 +}; + +function createDevnetWalletClients(rpcUrl: string): DevnetWalletClients { + const transport = http(rpcUrl); + const mkClient = (addressIndex: number) => + createWalletClient({ + chain: ensTestEnvChain, + transport, + account: mnemonicToAccount(DEVNET_MNEMONIC, { addressIndex }), + }); + return { + deployer: mkClient(0), + owner: mkClient(1), + user: mkClient(2), + user2: mkClient(3), + }; +} + +export async function seedDevnet(rpcUrl: string): Promise { + const clients = createDevnetWalletClients(rpcUrl); + await seedPrimaryNameRecords(clients); +} diff --git a/packages/integration-test-env/src/seed/primary-names.ts b/packages/integration-test-env/src/seed/primary-names.ts new file mode 100644 index 0000000000..8621bc6c0d --- /dev/null +++ b/packages/integration-test-env/src/seed/primary-names.ts @@ -0,0 +1,18 @@ +import { DEVNET_CONTRACTS } from "@ensnode/ensnode-sdk/internal"; + +import { ethReverseRegistrarAbi } from "./abi"; +import type { DevnetWalletClient, DevnetWalletClients } from "./index"; + +export async function seedPrimaryNameRecords(clients: DevnetWalletClients): Promise { + await setPrimaryNameRecord(clients.owner, "test.eth"); +} + +async function setPrimaryNameRecord(walletClient: DevnetWalletClient, name: string): Promise { + const hash = await walletClient.writeContract({ + address: DEVNET_CONTRACTS.ethReverseRegistrar, + abi: ethReverseRegistrarAbi, + functionName: "setName", + args: [name], + }); + console.log(`[seed] setPrimaryNameRecord("${name}") tx: ${hash}`); +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b6ed891c9a..cf36e6c7db 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1144,6 +1144,9 @@ importers: tsx: specifier: ^4.7.1 version: 4.20.6 + viem: + specifier: 'catalog:' + version: 2.38.5(typescript@5.9.3)(zod@4.3.6) packages/namehash-ui: dependencies: From 65f083f08de9bf34797183049b9173465081ecad Mon Sep 17 00:00:00 2001 From: sevenzing Date: Fri, 24 Apr 2026 17:15:15 +0400 Subject: [PATCH 02/18] add resolver records --- .../resolve-primary-name.integration.test.ts | 6 +- .../resolve-records.integration.test.ts | 93 ++++++++++- .../schema/account.integration.test.ts | 4 +- packages/ensnode-sdk/src/internal.ts | 1 + .../src/omnigraph-api/example-queries.ts | 1 - .../ensnode-sdk/src/shared/devnet/accounts.ts | 1 + .../src/shared/devnet/addresses.ts | 3 +- .../ensnode-sdk/src/shared/devnet/data.ts | 11 ++ packages/integration-test-env/src/seed/abi.ts | 17 ++ .../integration-test-env/src/seed/index.ts | 2 + .../src/seed/resolver-records.ts | 152 ++++++++++++++++++ 11 files changed, 286 insertions(+), 5 deletions(-) create mode 100644 packages/ensnode-sdk/src/shared/devnet/data.ts create mode 100644 packages/integration-test-env/src/seed/resolver-records.ts diff --git a/apps/ensapi/src/handlers/api/resolution/resolve-primary-name.integration.test.ts b/apps/ensapi/src/handlers/api/resolution/resolve-primary-name.integration.test.ts index d72606853d..c5f86e3aac 100644 --- a/apps/ensapi/src/handlers/api/resolution/resolve-primary-name.integration.test.ts +++ b/apps/ensapi/src/handlers/api/resolution/resolve-primary-name.integration.test.ts @@ -18,7 +18,11 @@ describe("GET /api/resolve/primary-name/:address/:chainId", () => { chainId: "1", query: "", expectedStatus: 200, - expectedBody: { name: "test.eth", accelerationRequested: false, accelerationAttempted: false }, + expectedBody: { + name: "test.eth", + accelerationRequested: false, + accelerationAttempted: false, + }, }, { description: diff --git a/apps/ensapi/src/handlers/api/resolution/resolve-records.integration.test.ts b/apps/ensapi/src/handlers/api/resolution/resolve-records.integration.test.ts index fe2e5ecdc1..52620389dd 100644 --- a/apps/ensapi/src/handlers/api/resolution/resolve-records.integration.test.ts +++ b/apps/ensapi/src/handlers/api/resolution/resolve-records.integration.test.ts @@ -6,7 +6,7 @@ import { describe, expect, it } from "vitest"; -import { DEVNET_ACCOUNTS } from "@ensnode/ensnode-sdk/internal"; +import { DEVNET_ACCOUNTS, DEVNET_BYTES } from "@ensnode/ensnode-sdk/internal"; const BASE_URL = process.env.ENSNODE_URL!; @@ -129,6 +129,97 @@ describe("GET /api/resolve/records/:name", () => { accelerationAttempted: false, }, }, + // -- Text records (seeded in devnet) -- + { + description: "resolves avatar text record for test.eth", + name: "test.eth", + query: "texts=avatar", + expectedStatus: 200, + expectedBody: { + records: { texts: { avatar: "https://example.com/avatar.png" } }, + accelerationRequested: false, + accelerationAttempted: false, + }, + }, + { + description: "returns null for unset text record", + name: "test.eth", + query: "texts=nonexistent.key", + expectedStatus: 200, + expectedBody: { + records: { texts: { "nonexistent.key": null } }, + accelerationRequested: false, + accelerationAttempted: false, + }, + }, + // -- Multi-coin addresses (seeded in devnet) -- + { + description: "resolves multiple coin types at once for test.eth", + name: "test.eth", + query: "addresses=60,0,2,777777", + expectedStatus: 200, + expectedBody: { + records: { + addresses: { + 60: DEVNET_ACCOUNTS.owner, + 0: DEVNET_BYTES.bitcoinAddress, + 2: DEVNET_BYTES.litecoinAddress, + 777777: null, + }, + }, + accelerationRequested: false, + accelerationAttempted: false, + }, + }, + // -- Combined records -- + { + description: "resolves every supported record type for test.eth", + name: "test.eth", + query: [ + "name=true", + "addresses=60,0,2", + "texts=avatar,description,url,email,com.twitter,com.github", + "contenthash=true", + "pubkey=true", + "version=true", + "abi=1", + `interfaces=${DEVNET_BYTES.fourBytesInterface}`, + ].join("&"), + expectedStatus: 200, + expectedBody: { + records: { + addresses: { + 60: DEVNET_ACCOUNTS.owner, + 0: DEVNET_BYTES.bitcoinAddress, + 2: DEVNET_BYTES.litecoinAddress, + }, + texts: { + avatar: "https://example.com/avatar.png", + description: "test.eth", + url: "https://ens.domains", + email: "test@ens.domains", + "com.twitter": "ensdomains", + "com.github": "ensdomains", + }, + contenthash: DEVNET_BYTES.contenthash, + pubkey: { + x: DEVNET_BYTES.publicKeyX, + y: DEVNET_BYTES.publicKeyY, + }, + version: expect.any(String), + abi: { + contentType: "1", + data: DEVNET_BYTES.abiBytes, + }, + interfaces: { + [DEVNET_BYTES.fourBytesInterface]: DEVNET_ACCOUNTS.one, + }, + }, + accelerationRequested: false, + accelerationAttempted: false, + }, + }, + // -- Acceleration -- { description: "test.eth with accelerate=true returns accelerationRequested: true", name: "test.eth", 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 8ae5d71b7f..57a78754ae 100644 --- a/apps/ensapi/src/omnigraph-api/schema/account.integration.test.ts +++ b/apps/ensapi/src/omnigraph-api/schema/account.integration.test.ts @@ -36,7 +36,9 @@ describe("Account.domains", () => { `; it("returns domains owned by the devnet owner", async () => { - const result = await request(AccountDomains, { address: DEVNET_ACCOUNTS.owner }); + const result = await request(AccountDomains, { + address: DEVNET_ACCOUNTS.owner, + }); const domains = flattenConnection(result.account.domains); const names = domains.map((d) => d.name); diff --git a/packages/ensnode-sdk/src/internal.ts b/packages/ensnode-sdk/src/internal.ts index 97786b03b1..f348e122e6 100644 --- a/packages/ensnode-sdk/src/internal.ts +++ b/packages/ensnode-sdk/src/internal.ts @@ -41,6 +41,7 @@ export * from "./shared/datasources-with-ensv2-contracts"; export * from "./shared/datasources-with-resolvers"; export * from "./shared/devnet/accounts"; export * from "./shared/devnet/addresses"; +export * from "./shared/devnet/data"; export * from "./shared/interpretation/interpret-address"; export * from "./shared/interpretation/interpret-record-values"; export * from "./shared/interpretation/interpret-resolver-values"; diff --git a/packages/ensnode-sdk/src/omnigraph-api/example-queries.ts b/packages/ensnode-sdk/src/omnigraph-api/example-queries.ts index 9051f63d76..749a221930 100644 --- a/packages/ensnode-sdk/src/omnigraph-api/example-queries.ts +++ b/packages/ensnode-sdk/src/omnigraph-api/example-queries.ts @@ -30,7 +30,6 @@ const ENS_TEST_ENV_V2_ETH_REGISTRAR = maybeGetDatasourceContract( "ETHRegistrar", ); - const VITALIK_ADDRESS = toNormalizedAddress("0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045"); const DEVNET_NAME_WITH_OWNED_RESOLVER = asInterpretedName("example.eth"); diff --git a/packages/ensnode-sdk/src/shared/devnet/accounts.ts b/packages/ensnode-sdk/src/shared/devnet/accounts.ts index 90573430f0..65ebd4ee35 100644 --- a/packages/ensnode-sdk/src/shared/devnet/accounts.ts +++ b/packages/ensnode-sdk/src/shared/devnet/accounts.ts @@ -23,4 +23,5 @@ export const DEVNET_ACCOUNTS = { user: toNormalizedAddress("0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC"), /** Account index 3 */ user2: toNormalizedAddress("0x90F79bf6EB2c4f870365E785982E1f101E93b906"), + one: toNormalizedAddress(`0x${"1".repeat(40)}`), } as const; diff --git a/packages/ensnode-sdk/src/shared/devnet/addresses.ts b/packages/ensnode-sdk/src/shared/devnet/addresses.ts index eb6b5c7282..147be5e37c 100644 --- a/packages/ensnode-sdk/src/shared/devnet/addresses.ts +++ b/packages/ensnode-sdk/src/shared/devnet/addresses.ts @@ -45,9 +45,10 @@ export const DEVNET_CONTRACTS = { ensv1Resolver: "0x5FC8d32690cc91D4c39d9d3abcBD16989F875707", ensv2Resolver: "0xc6e7DF5E7b4f2A278906862b61205850344D4e7d", ownedResolver: "0x68B1D87F95878fE05B998F19b66F4baba5De1aed", + permissionedResolver: "0x5eA90aCF6555276660760fE629D72932c91f4b8E", legacyPublicResolver: "0x86A2EE8FAf9A840F7a2c64CA3d51209F9A02081D", publicResolver: "0xA4899D35897033b927acFCf422bc745916139776", - permissionedResolver: "0x809d550fca64d94Bd9F66E60752A544199cfAC3D", + permissionedResolverImpl: "0x809d550fca64d94Bd9F66E60752A544199cfAC3D", universalResolver: "0x5067457698Fd6Fa1C6964e416b3f42713513B3dD", universalResolverV2: "0x8198f5d8F8CfFE8f9C413d98a0A55aEB8ab9FbB7", upgradableUniversalResolverProxy: "0x0355B7B8cb128fA5692729Ab3AAa199C1753f726", diff --git a/packages/ensnode-sdk/src/shared/devnet/data.ts b/packages/ensnode-sdk/src/shared/devnet/data.ts new file mode 100644 index 0000000000..2dd590afff --- /dev/null +++ b/packages/ensnode-sdk/src/shared/devnet/data.ts @@ -0,0 +1,11 @@ +import type { Hex } from "viem"; + +export const DEVNET_BYTES: Record = { + abiBytes: `0x${"01".repeat(32)}` as Hex, + fourBytesInterface: "0x11100111" as Hex, + publicKeyX: `0x${"02".repeat(32)}` as Hex, + publicKeyY: `0x${"03".repeat(32)}` as Hex, + contenthash: `0x${"04".repeat(32)}` as Hex, + bitcoinAddress: `0x${"05".repeat(25)}` as Hex, + litecoinAddress: `0x${"06".repeat(25)}` as Hex, +}; diff --git a/packages/integration-test-env/src/seed/abi.ts b/packages/integration-test-env/src/seed/abi.ts index bc251ab7d3..16e8ef3afa 100644 --- a/packages/integration-test-env/src/seed/abi.ts +++ b/packages/integration-test-env/src/seed/abi.ts @@ -4,3 +4,20 @@ export const ethReverseRegistrarAbi = parseAbi([ // https://github.com/ensdomains/contracts-v2/blob/42f2016e7ba87eb3854afe51ad55186a16b32a74/contracts/src/reverse-registrar/L2ReverseRegistrar.sol#L117-L119 "function setName(string name) returns (bytes32)", ]); + +// https://github.com/ensdomains/contracts-v2/blob/42f2016e7ba87eb3854afe51ad55186a16b32a74/contracts/test/utils/resolutions.ts#L28 +export const publicResolverAbi = parseAbi([ + "function setName(bytes32 node, string newName)", + "function setText(bytes32 node, string key, string value)", + "function setAddr(bytes32 node, address a)", + "function setAddr(bytes32 node, uint256 coinType, bytes a)", + "function setContenthash(bytes32 node, bytes hash)", + "function setPubkey(bytes32 node, bytes32 x, bytes32 y)", + "function setABI(bytes32 node, uint256 contentType, bytes data)", + "function setInterface(bytes32 node, bytes4 interfaceID, address implementer)", + "function clearRecords(bytes32 node)", +]); + +export const universalResolverV2Abi = parseAbi([ + "function findResolver(bytes name) view returns (address resolver, bytes32 node, uint256 offset)", +]); diff --git a/packages/integration-test-env/src/seed/index.ts b/packages/integration-test-env/src/seed/index.ts index f1aab1f3f5..b9ddef4b5a 100644 --- a/packages/integration-test-env/src/seed/index.ts +++ b/packages/integration-test-env/src/seed/index.ts @@ -12,6 +12,7 @@ import { ensTestEnvChain } from "@ensnode/datasources"; import { DEVNET_MNEMONIC } from "@ensnode/ensnode-sdk/internal"; import { seedPrimaryNameRecords } from "./primary-names"; +import { seedResolverRecords } from "./resolver-records"; export type DevnetWalletClient = WalletClient; @@ -41,4 +42,5 @@ function createDevnetWalletClients(rpcUrl: string): DevnetWalletClients { export async function seedDevnet(rpcUrl: string): Promise { const clients = createDevnetWalletClients(rpcUrl); await seedPrimaryNameRecords(clients); + await seedResolverRecords(clients); } diff --git a/packages/integration-test-env/src/seed/resolver-records.ts b/packages/integration-test-env/src/seed/resolver-records.ts new file mode 100644 index 0000000000..0d1124741f --- /dev/null +++ b/packages/integration-test-env/src/seed/resolver-records.ts @@ -0,0 +1,152 @@ +import { type Address, createPublicClient, type Hex, http, namehash, toHex } from "viem"; +import { packetToBytes } from "viem/ens"; + +import { ensTestEnvChain } from "@ensnode/datasources"; +import { DEVNET_ACCOUNTS, DEVNET_BYTES, DEVNET_CONTRACTS } from "@ensnode/ensnode-sdk/internal"; + +import { publicResolverAbi, universalResolverV2Abi } from "./abi"; +import type { DevnetWalletClient, DevnetWalletClients } from "./index"; + +const RESOLVER = DEVNET_CONTRACTS.permissionedResolver; + +export async function seedResolverRecords(clients: DevnetWalletClients): Promise { + const node = namehash("test.eth"); + await assertTestEthResolver(clients.owner.transport.url); + + // Text records + await setTextRecord(clients.owner, node, "avatar", "https://example.com/avatar.png"); + await setTextRecord(clients.owner, node, "com.twitter", "ensdomains"); + await setTextRecord(clients.owner, node, "com.github", "ensdomains"); + await setTextRecord(clients.owner, node, "url", "https://ens.domains"); + await setTextRecord(clients.owner, node, "email", "test@ens.domains"); + await setTextRecord(clients.owner, node, "description", "test.eth"); + + // Multi-coin addresses + // Coin 0 = Bitcoin + await setMulticoinAddress(clients.owner, node, 0n, DEVNET_BYTES.bitcoinAddress); + // Coin 2 = Litecoin + await setMulticoinAddress(clients.owner, node, 2n, DEVNET_BYTES.litecoinAddress); + + // Scalar resolver records + await setContenthash(clients.owner, node, DEVNET_BYTES.contenthash); + await setPubkey(clients.owner, node, DEVNET_BYTES.publicKeyX, DEVNET_BYTES.publicKeyY); + await setAbi(clients.owner, node, 1n, DEVNET_BYTES.abiBytes); + await setInterfaceImplementer( + clients.owner, + node, + DEVNET_BYTES.fourBytesInterface, + DEVNET_ACCOUNTS.one, + ); +} + +async function assertTestEthResolver(rpcUrl: string): Promise { + const publicClient = createPublicClient({ chain: ensTestEnvChain, transport: http(rpcUrl) }); + const [activeResolver] = await publicClient.readContract({ + address: DEVNET_CONTRACTS.universalResolverV2, + abi: universalResolverV2Abi, + functionName: "findResolver", + args: [toHex(packetToBytes("test.eth"))], + }); + if (activeResolver.toLowerCase() !== RESOLVER.toLowerCase()) { + throw new Error(`test.eth resolver mismatch: active=${activeResolver}, expected=${RESOLVER}`); + } +} + +async function setTextRecord( + walletClient: DevnetWalletClient, + node: Hex, + key: string, + value: string, +): Promise { + const hash = await walletClient.writeContract({ + address: RESOLVER, + abi: publicResolverAbi, + functionName: "setText", + args: [node, key, value], + }); + console.log(`[seed] setText("${key}", "${value}") tx: ${hash}`); +} + +async function setMulticoinAddress( + walletClient: DevnetWalletClient, + node: Hex, + coinType: bigint, + addressBytes: Hex, +): Promise { + const hash = await walletClient.writeContract({ + address: RESOLVER, + abi: publicResolverAbi, + functionName: "setAddr", + args: [node, coinType, addressBytes], + }); + console.log(`[seed] setAddr(coinType=${coinType}) tx: ${hash}`); +} + +async function setContenthash( + walletClient: DevnetWalletClient, + node: Hex, + hashValue: Hex, +): Promise { + const hash = await walletClient.writeContract({ + address: RESOLVER, + abi: publicResolverAbi, + functionName: "setContenthash", + args: [node, hashValue], + }); + console.log(`[seed] setContenthash() tx: ${hash}`); +} + +async function setPubkey( + walletClient: DevnetWalletClient, + node: Hex, + x: Hex, + y: Hex, +): Promise { + const hash = await walletClient.writeContract({ + address: RESOLVER, + abi: publicResolverAbi, + functionName: "setPubkey", + args: [node, x, y], + }); + console.log(`[seed] setPubkey() tx: ${hash}`); +} + +async function setAbi( + walletClient: DevnetWalletClient, + node: Hex, + contentType: bigint, + data: Hex, +): Promise { + const hash = await walletClient.writeContract({ + address: RESOLVER, + abi: publicResolverAbi, + functionName: "setABI", + args: [node, contentType, data], + }); + console.log(`[seed] setABI(contentType=${contentType}) tx: ${hash}`); +} + +async function setInterfaceImplementer( + walletClient: DevnetWalletClient, + node: Hex, + interfaceId: Hex, + implementer: Address, +): Promise { + const hash = await walletClient.writeContract({ + address: RESOLVER, + abi: publicResolverAbi, + functionName: "setInterface", + args: [node, interfaceId, implementer], + }); + console.log(`[seed] setInterface(interfaceId=${interfaceId}) tx: ${hash}`); +} + +// async function clearResolverRecords(walletClient: DevnetWalletClient, node: Hex): Promise { +// const hash = await walletClient.writeContract({ +// address: PUBLIC_RESOLVER, +// abi: publicResolverAbi, +// functionName: "clearRecords", +// args: [node], +// }); +// console.log(`[seed] clearRecords() tx: ${hash}`); +// } From b5037aa1158e559a69316beb9f76fc4dcce40681 Mon Sep 17 00:00:00 2001 From: sevenzing Date: Fri, 24 Apr 2026 17:29:29 +0400 Subject: [PATCH 03/18] remove used code --- .../integration-test-env/src/seed/resolver-records.ts | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/packages/integration-test-env/src/seed/resolver-records.ts b/packages/integration-test-env/src/seed/resolver-records.ts index 0d1124741f..b8823158ed 100644 --- a/packages/integration-test-env/src/seed/resolver-records.ts +++ b/packages/integration-test-env/src/seed/resolver-records.ts @@ -140,13 +140,3 @@ async function setInterfaceImplementer( }); console.log(`[seed] setInterface(interfaceId=${interfaceId}) tx: ${hash}`); } - -// async function clearResolverRecords(walletClient: DevnetWalletClient, node: Hex): Promise { -// const hash = await walletClient.writeContract({ -// address: PUBLIC_RESOLVER, -// abi: publicResolverAbi, -// functionName: "clearRecords", -// args: [node], -// }); -// console.log(`[seed] clearRecords() tx: ${hash}`); -// } From 61803ee67f38fc1909b0c3ffa056e33c454f043d Mon Sep 17 00:00:00 2001 From: sevenzing Date: Sat, 25 Apr 2026 20:12:27 +0400 Subject: [PATCH 04/18] fix PR comments --- .../resolve-primary-name.integration.test.ts | 11 ++- .../resolve-primary-names.integration.test.ts | 11 ++- .../resolve-records.integration.test.ts | 22 ++--- .../schema/account.integration.test.ts | 30 +++---- packages/datasources/src/lib/chains.ts | 5 +- packages/ensnode-sdk/src/internal.ts | 1 - .../src/omnigraph-api/example-queries.ts | 10 +-- .../ensnode-sdk/src/shared/devnet/accounts.ts | 27 ------ .../src/shared/devnet/addresses.ts | 27 ++++++ .../ensnode-sdk/src/shared/devnet/data.ts | 21 +++-- .../integration-test-env/src/orchestrator.ts | 12 +-- .../integration-test-env/src/seed/index.ts | 35 +++++--- .../src/seed/resolver-records.ts | 89 ++++++++++++------- 13 files changed, 165 insertions(+), 136 deletions(-) delete mode 100644 packages/ensnode-sdk/src/shared/devnet/accounts.ts diff --git a/apps/ensapi/src/handlers/api/resolution/resolve-primary-name.integration.test.ts b/apps/ensapi/src/handlers/api/resolution/resolve-primary-name.integration.test.ts index c5f86e3aac..d5597ce660 100644 --- a/apps/ensapi/src/handlers/api/resolution/resolve-primary-name.integration.test.ts +++ b/apps/ensapi/src/handlers/api/resolution/resolve-primary-name.integration.test.ts @@ -14,7 +14,7 @@ describe("GET /api/resolve/primary-name/:address/:chainId", () => { it.each([ { description: "resolves primary name for owner address on chain 1", - address: DEVNET_ACCOUNTS.owner, + address: DEVNET_ACCOUNTS.owner.address, chainId: "1", query: "", expectedStatus: 200, @@ -25,9 +25,8 @@ describe("GET /api/resolve/primary-name/:address/:chainId", () => { }, }, { - description: - "resolves primary name for user address on chain 1 (no primary name set in devnet)", - address: DEVNET_ACCOUNTS.user, + description: "returns null for user without a primary name", + address: DEVNET_ACCOUNTS.user.address, chainId: "1", query: "", expectedStatus: 200, @@ -35,7 +34,7 @@ describe("GET /api/resolve/primary-name/:address/:chainId", () => { }, { description: "owner address with accelerate=true returns accelerationRequested: true", - address: DEVNET_ACCOUNTS.owner, + address: DEVNET_ACCOUNTS.owner.address, chainId: "1", query: "accelerate=true", expectedStatus: 200, @@ -61,7 +60,7 @@ describe("GET /api/resolve/primary-name/:address/:chainId", () => { }, { description: "returns 400 for non-numeric chainId", - address: DEVNET_ACCOUNTS.owner, + address: DEVNET_ACCOUNTS.owner.address, chainId: "notachainid", query: "", expectedStatus: 400, diff --git a/apps/ensapi/src/handlers/api/resolution/resolve-primary-names.integration.test.ts b/apps/ensapi/src/handlers/api/resolution/resolve-primary-names.integration.test.ts index 06cf4d7af6..6fdcd204e8 100644 --- a/apps/ensapi/src/handlers/api/resolution/resolve-primary-names.integration.test.ts +++ b/apps/ensapi/src/handlers/api/resolution/resolve-primary-names.integration.test.ts @@ -13,9 +13,8 @@ const BASE_URL = process.env.ENSNODE_URL!; describe("GET /api/resolve/primary-names/:address", () => { it.each([ { - description: - "resolves primary names for owner address on chain 1 (no primary name set in devnet)", - address: DEVNET_ACCOUNTS.owner, + description: "resolves primary names for owner address on chain 1", + address: DEVNET_ACCOUNTS.owner.address, query: "chainIds=1", expectedStatus: 200, expectedBody: { @@ -26,7 +25,7 @@ describe("GET /api/resolve/primary-names/:address", () => { }, { description: "resolves all primary names", - address: DEVNET_ACCOUNTS.owner, + address: DEVNET_ACCOUNTS.owner.address, query: "", expectedStatus: 200, expectedBody: { @@ -54,7 +53,7 @@ describe("GET /api/resolve/primary-names/:address", () => { }, { description: "returns 400 when chainIds contains the default chain id (0)", - address: DEVNET_ACCOUNTS.owner, + address: DEVNET_ACCOUNTS.owner.address, query: "chainIds=0", expectedStatus: 400, expectedBody: { @@ -76,7 +75,7 @@ describe("GET /api/resolve/primary-names/:address", () => { }, { description: "returns 400 when chainIds contains duplicate chain ids", - address: DEVNET_ACCOUNTS.owner, + address: DEVNET_ACCOUNTS.owner.address, query: "chainIds=1,1", expectedStatus: 400, expectedBody: { diff --git a/apps/ensapi/src/handlers/api/resolution/resolve-records.integration.test.ts b/apps/ensapi/src/handlers/api/resolution/resolve-records.integration.test.ts index 52620389dd..47f00cbf61 100644 --- a/apps/ensapi/src/handlers/api/resolution/resolve-records.integration.test.ts +++ b/apps/ensapi/src/handlers/api/resolution/resolve-records.integration.test.ts @@ -6,7 +6,7 @@ import { describe, expect, it } from "vitest"; -import { DEVNET_ACCOUNTS, DEVNET_BYTES } from "@ensnode/ensnode-sdk/internal"; +import { DEVNET_ACCOUNTS, DEVNET_ADDRESSES, DEVNET_BYTES } from "@ensnode/ensnode-sdk/internal"; const BASE_URL = process.env.ENSNODE_URL!; @@ -18,7 +18,7 @@ describe("GET /api/resolve/records/:name", () => { query: "addresses=60", expectedStatus: 200, expectedBody: { - records: { addresses: { 60: DEVNET_ACCOUNTS.owner } }, + records: { addresses: { 60: DEVNET_ACCOUNTS.owner.address } }, accelerationRequested: false, accelerationAttempted: false, }, @@ -30,7 +30,7 @@ describe("GET /api/resolve/records/:name", () => { query: "addresses=60", expectedStatus: 200, expectedBody: { - records: { addresses: { 60: DEVNET_ACCOUNTS.owner } }, + records: { addresses: { 60: DEVNET_ACCOUNTS.owner.address } }, accelerationRequested: false, accelerationAttempted: false, }, @@ -65,7 +65,7 @@ describe("GET /api/resolve/records/:name", () => { expectedStatus: 200, expectedBody: { records: { - addresses: { 60: DEVNET_ACCOUNTS.owner }, + addresses: { 60: DEVNET_ACCOUNTS.owner.address }, texts: { description: "example.eth" }, }, accelerationRequested: false, @@ -90,7 +90,7 @@ describe("GET /api/resolve/records/:name", () => { query: "addresses=60", expectedStatus: 200, expectedBody: { - records: { addresses: { 60: DEVNET_ACCOUNTS.owner } }, + records: { addresses: { 60: DEVNET_ACCOUNTS.owner.address } }, accelerationRequested: false, accelerationAttempted: false, }, @@ -112,7 +112,7 @@ describe("GET /api/resolve/records/:name", () => { query: "addresses=60", expectedStatus: 200, expectedBody: { - records: { addresses: { 60: DEVNET_ACCOUNTS.owner } }, + records: { addresses: { 60: DEVNET_ACCOUNTS.owner.address } }, accelerationRequested: false, accelerationAttempted: false, }, @@ -124,7 +124,7 @@ describe("GET /api/resolve/records/:name", () => { query: "addresses=60", expectedStatus: 200, expectedBody: { - records: { addresses: { 60: DEVNET_ACCOUNTS.owner } }, + records: { addresses: { 60: DEVNET_ACCOUNTS.owner.address } }, accelerationRequested: false, accelerationAttempted: false, }, @@ -161,7 +161,7 @@ describe("GET /api/resolve/records/:name", () => { expectedBody: { records: { addresses: { - 60: DEVNET_ACCOUNTS.owner, + 60: DEVNET_ACCOUNTS.owner.address, 0: DEVNET_BYTES.bitcoinAddress, 2: DEVNET_BYTES.litecoinAddress, 777777: null, @@ -189,7 +189,7 @@ describe("GET /api/resolve/records/:name", () => { expectedBody: { records: { addresses: { - 60: DEVNET_ACCOUNTS.owner, + 60: DEVNET_ACCOUNTS.owner.address, 0: DEVNET_BYTES.bitcoinAddress, 2: DEVNET_BYTES.litecoinAddress, }, @@ -212,7 +212,7 @@ describe("GET /api/resolve/records/:name", () => { data: DEVNET_BYTES.abiBytes, }, interfaces: { - [DEVNET_BYTES.fourBytesInterface]: DEVNET_ACCOUNTS.one, + [DEVNET_BYTES.fourBytesInterface]: DEVNET_ADDRESSES.one, }, }, accelerationRequested: false, @@ -226,7 +226,7 @@ describe("GET /api/resolve/records/:name", () => { query: "addresses=60&accelerate=true", expectedStatus: 200, expectedBody: { - records: { addresses: { 60: DEVNET_ACCOUNTS.owner } }, + records: { addresses: { 60: DEVNET_ACCOUNTS.owner.address } }, accelerationRequested: true, accelerationAttempted: false, }, 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 57a78754ae..8dfe525c48 100644 --- a/apps/ensapi/src/omnigraph-api/schema/account.integration.test.ts +++ b/apps/ensapi/src/omnigraph-api/schema/account.integration.test.ts @@ -37,7 +37,7 @@ describe("Account.domains", () => { it("returns domains owned by the devnet owner", async () => { const result = await request(AccountDomains, { - address: DEVNET_ACCOUNTS.owner, + address: DEVNET_ACCOUNTS.owner.address, }); const domains = flattenConnection(result.account.domains); const names = domains.map((d) => d.name); @@ -64,7 +64,7 @@ describe("Account.domains", () => { it("returns domains owned by the new owner", async () => { const result = await request(AccountDomains, { - address: DEVNET_ACCOUNTS.user, + address: DEVNET_ACCOUNTS.user.address, }); const domains = flattenConnection(result.account.domains); const names = domains.map((d) => d.name); @@ -77,7 +77,7 @@ describe("Account.domains pagination", () => { testDomainPagination(async (variables) => { const result = await request<{ account: { domains: PaginatedGraphQLConnection }; - }>(AccountDomainsPaginated, { address: DEVNET_ACCOUNTS.owner, ...variables }); + }>(AccountDomainsPaginated, { address: DEVNET_ACCOUNTS.owner.address, ...variables }); return result.account.domains; }); }); @@ -94,14 +94,14 @@ describe("Account.events", () => { it("returns events for the devnet deployer", async () => { const result = await request(AccountEvents, { - address: DEVNET_ACCOUNTS.deployer, + address: DEVNET_ACCOUNTS.deployer.address, }); const events = flattenConnection(result.account.events); expect(events.length).toBeGreaterThan(0); for (const event of events) { - expect(event.from).toBe(DEVNET_ACCOUNTS.deployer); + expect(event.from).toBe(DEVNET_ACCOUNTS.deployer.address); } }); }); @@ -110,7 +110,7 @@ describe("Account.events pagination", () => { testEventPagination(async (variables) => { const result = await request<{ account: { events: PaginatedGraphQLConnection }; - }>(AccountEventsPaginated, { address: DEVNET_ACCOUNTS.deployer, ...variables }); + }>(AccountEventsPaginated, { address: DEVNET_ACCOUNTS.deployer.address, ...variables }); return result.account.events; }); }); @@ -129,7 +129,7 @@ describe("Account.events filtering (AccountEventsWhereInput)", () => { beforeAll(async () => { const result = await request(AccountEventsFiltered, { - address: DEVNET_ACCOUNTS.deployer, + address: DEVNET_ACCOUNTS.deployer.address, first: 1000, }); // events are returned in ascending order, so first/last access yields min/max values @@ -141,7 +141,7 @@ describe("Account.events filtering (AccountEventsWhereInput)", () => { const targetSelector = allEvents[0].topics[0]; const result = await request(AccountEventsFiltered, { - address: DEVNET_ACCOUNTS.deployer, + address: DEVNET_ACCOUNTS.deployer.address, where: { selector_in: [targetSelector] }, }); const events = flattenConnection(result.account.events); @@ -154,7 +154,7 @@ describe("Account.events filtering (AccountEventsWhereInput)", () => { it("filters by selector_in with unknown topic returns no results", async () => { const result = await request(AccountEventsFiltered, { - address: DEVNET_ACCOUNTS.deployer, + address: DEVNET_ACCOUNTS.deployer.address, where: { selector_in: ["0x0000000000000000000000000000000000000000000000000000000000000001"], }, @@ -165,7 +165,7 @@ describe("Account.events filtering (AccountEventsWhereInput)", () => { it("filters by empty selector_in returns no results", async () => { const result = await request(AccountEventsFiltered, { - address: DEVNET_ACCOUNTS.deployer, + address: DEVNET_ACCOUNTS.deployer.address, where: { selector_in: [] }, }); const events = flattenConnection(result.account.events); @@ -176,7 +176,7 @@ describe("Account.events filtering (AccountEventsWhereInput)", () => { const midTimestamp = allEvents[Math.floor(allEvents.length / 2)].timestamp; const result = await request(AccountEventsFiltered, { - address: DEVNET_ACCOUNTS.deployer, + address: DEVNET_ACCOUNTS.deployer.address, where: { timestamp_gte: midTimestamp }, }); const events = flattenConnection(result.account.events); @@ -192,7 +192,7 @@ describe("Account.events filtering (AccountEventsWhereInput)", () => { const midTimestamp = allEvents[Math.floor(allEvents.length / 2)].timestamp; const result = await request(AccountEventsFiltered, { - address: DEVNET_ACCOUNTS.deployer, + address: DEVNET_ACCOUNTS.deployer.address, where: { timestamp_lte: midTimestamp }, }); const events = flattenConnection(result.account.events); @@ -209,7 +209,7 @@ describe("Account.events filtering (AccountEventsWhereInput)", () => { const maxTs = allEvents[allEvents.length - 1].timestamp; const result = await request(AccountEventsFiltered, { - address: DEVNET_ACCOUNTS.deployer, + address: DEVNET_ACCOUNTS.deployer.address, where: { timestamp_gte: minTs, timestamp_lte: maxTs }, first: 1000, }); @@ -226,7 +226,7 @@ describe("Account.events filtering (AccountEventsWhereInput)", () => { const midTimestamp = seedEvent.timestamp; const result = await request(AccountEventsFiltered, { - address: DEVNET_ACCOUNTS.deployer, + address: DEVNET_ACCOUNTS.deployer.address, where: { selector_in: [targetSelector], timestamp_gte: midTimestamp }, }); const events = flattenConnection(result.account.events); @@ -243,7 +243,7 @@ describe("Account.events filtering (AccountEventsWhereInput)", () => { const maxTimestamp = BigInt(allEvents[allEvents.length - 1].timestamp); const result = await request(AccountEventsFiltered, { - address: DEVNET_ACCOUNTS.deployer, + address: DEVNET_ACCOUNTS.deployer.address, where: { timestamp_gte: (maxTimestamp + 1n).toString() }, }); const events = flattenConnection(result.account.events); diff --git a/packages/datasources/src/lib/chains.ts b/packages/datasources/src/lib/chains.ts index 736c1c4485..cc019cc31c 100644 --- a/packages/datasources/src/lib/chains.ts +++ b/packages/datasources/src/lib/chains.ts @@ -1,14 +1,13 @@ import { type Chain, localhost } from "viem/chains"; /** - * The Default Chain Id for Devnet + * The ens-test-env chain id is 1: * @see https://github.com/ensdomains/contracts-v2/blob/762de44d60b2588b2e92a6d29df941c4de821ae6/contracts/script/setup.ts#L40 */ -const DEVNET_DEFAULT_CHAIN_ID = 1; export const ensTestEnvChain = { ...localhost, - id: DEVNET_DEFAULT_CHAIN_ID, + id: 1, name: "ens-test-env", rpcUrls: { default: { http: ["http://localhost:8545"] } }, } as const satisfies Chain; diff --git a/packages/ensnode-sdk/src/internal.ts b/packages/ensnode-sdk/src/internal.ts index f348e122e6..997a2bef8c 100644 --- a/packages/ensnode-sdk/src/internal.ts +++ b/packages/ensnode-sdk/src/internal.ts @@ -39,7 +39,6 @@ export * from "./shared/config/zod-schemas"; export * from "./shared/config-templates"; export * from "./shared/datasources-with-ensv2-contracts"; export * from "./shared/datasources-with-resolvers"; -export * from "./shared/devnet/accounts"; export * from "./shared/devnet/addresses"; export * from "./shared/devnet/data"; export * from "./shared/interpretation/interpret-address"; diff --git a/packages/ensnode-sdk/src/omnigraph-api/example-queries.ts b/packages/ensnode-sdk/src/omnigraph-api/example-queries.ts index 749a221930..a48842e523 100644 --- a/packages/ensnode-sdk/src/omnigraph-api/example-queries.ts +++ b/packages/ensnode-sdk/src/omnigraph-api/example-queries.ts @@ -3,7 +3,7 @@ import { asInterpretedName, toNormalizedAddress } from "enssdk"; import { DatasourceNames, ENSNamespaceIds } from "@ensnode/datasources"; import { maybeGetDatasourceContract } from "../shared/datasource-contract"; -import { DEVNET_ACCOUNTS } from "../shared/devnet/accounts"; +import { DEVNET_ACCOUNTS } from "../shared/devnet/addresses"; import type { NamespaceSpecificValue } from "../shared/namespace-specific-value"; const SEPOLIA_V2_V2_ETH_REGISTRY = maybeGetDatasourceContract( @@ -181,7 +181,7 @@ query AccountDomains( }`, variables: { default: { address: VITALIK_ADDRESS }, - [ENSNamespaceIds.EnsTestEnv]: { address: DEVNET_ACCOUNTS.owner }, + [ENSNamespaceIds.EnsTestEnv]: { address: DEVNET_ACCOUNTS.owner.address }, }, }, @@ -199,7 +199,7 @@ query AccountEvents( }`, variables: { default: { address: VITALIK_ADDRESS }, - [ENSNamespaceIds.EnsTestEnv]: { address: DEVNET_ACCOUNTS.deployer }, + [ENSNamespaceIds.EnsTestEnv]: { address: DEVNET_ACCOUNTS.deployer.address }, }, }, @@ -282,7 +282,7 @@ query PermissionsByUser($address: Address!) { } }`, variables: { - default: { address: DEVNET_ACCOUNTS.deployer }, + default: { address: DEVNET_ACCOUNTS.deployer.address }, // TODO: figure out a good sepolia-v2 user address // [ENSNamespaceIds.SepoliaV2]: { address: "" }, }, @@ -309,7 +309,7 @@ query AccountResolverPermissions($address: Address!) { } }`, variables: { - default: { address: DEVNET_ACCOUNTS.deployer }, + default: { address: DEVNET_ACCOUNTS.deployer.address }, // TODO: figure out a good sepolia-v2 user address // [ENSNamespaceIds.SepoliaV2]: { address: "" }, }, diff --git a/packages/ensnode-sdk/src/shared/devnet/accounts.ts b/packages/ensnode-sdk/src/shared/devnet/accounts.ts deleted file mode 100644 index 65ebd4ee35..0000000000 --- a/packages/ensnode-sdk/src/shared/devnet/accounts.ts +++ /dev/null @@ -1,27 +0,0 @@ -/** - * Named accounts from the ens-test-env devnet. - * Derived from the standard Hardhat mnemonic at account indices 0–3. - * They are NOT real Ethereum Mainnet or testnet addresses. - * - * @see https://github.com/ensdomains/ens-test-env - * @see https://github.com/ensdomains/contracts-v2/blob/42f2016e7ba87eb3854afe51ad55186a16b32a74/contracts/script/setup.ts#L55 - */ - -import { toNormalizedAddress } from "enssdk"; - -/** - * Standard Hardhat/Anvil mnemonic used by the ens-test-env devnet. - */ -export const DEVNET_MNEMONIC = "test test test test test test test test test test test junk"; - -export const DEVNET_ACCOUNTS = { - /** Account index 0 — has REGISTRAR role on ETHRegistry */ - deployer: toNormalizedAddress("0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266"), - /** Account index 1 — owns test.eth */ - owner: toNormalizedAddress("0x70997970c51812dc3a010c7d01b50e0d17dc79c8"), - /** Account index 2 */ - user: toNormalizedAddress("0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC"), - /** Account index 3 */ - user2: toNormalizedAddress("0x90F79bf6EB2c4f870365E785982E1f101E93b906"), - one: toNormalizedAddress(`0x${"1".repeat(40)}`), -} as const; diff --git a/packages/ensnode-sdk/src/shared/devnet/addresses.ts b/packages/ensnode-sdk/src/shared/devnet/addresses.ts index 147be5e37c..3349e31c53 100644 --- a/packages/ensnode-sdk/src/shared/devnet/addresses.ts +++ b/packages/ensnode-sdk/src/shared/devnet/addresses.ts @@ -1,3 +1,6 @@ +import { toNormalizedAddress } from "enssdk"; +import { mnemonicToAccount } from "viem/accounts"; + /** * Deterministic contract addresses from the ens-test-env devnet. * These addresses are produced by the Hardhat/Anvil deploy scripts in contracts-v2 @@ -95,3 +98,27 @@ export const DEVNET_CONTRACTS = { mockUsdc: "0xFD471836031dc5108809D173A067e8486B9047A3", mockDai: "0xcbEAF3BDe82155F56486Fb5a1072cb8baAf547cc", } as const; + +/** + * Standard Hardhat/Anvil mnemonic used by the ens-test-env devnet. + */ +const DEVNET_MNEMONIC = "test test test test test test test test test test test junk"; + +/** + * Named signer accounts from the ens-test-env devnet. + * Derived from the standard Hardhat mnemonic at account indices 0-3. + */ +export const DEVNET_ACCOUNTS = { + deployer: mnemonicToAccount(DEVNET_MNEMONIC, { addressIndex: 0 }), + owner: mnemonicToAccount(DEVNET_MNEMONIC, { addressIndex: 1 }), + user: mnemonicToAccount(DEVNET_MNEMONIC, { addressIndex: 2 }), + user2: mnemonicToAccount(DEVNET_MNEMONIC, { addressIndex: 3 }), +} as const; + +/** + * Non-signer helper addresses for tests. + */ +export const DEVNET_ADDRESSES = { + /** Non-signer placeholder address (0x11...11) used where only an address literal is needed. */ + one: toNormalizedAddress(`0x${"1".repeat(40)}`), +} as const; diff --git a/packages/ensnode-sdk/src/shared/devnet/data.ts b/packages/ensnode-sdk/src/shared/devnet/data.ts index 2dd590afff..f974dfcd47 100644 --- a/packages/ensnode-sdk/src/shared/devnet/data.ts +++ b/packages/ensnode-sdk/src/shared/devnet/data.ts @@ -1,11 +1,14 @@ import type { Hex } from "viem"; -export const DEVNET_BYTES: Record = { - abiBytes: `0x${"01".repeat(32)}` as Hex, - fourBytesInterface: "0x11100111" as Hex, - publicKeyX: `0x${"02".repeat(32)}` as Hex, - publicKeyY: `0x${"03".repeat(32)}` as Hex, - contenthash: `0x${"04".repeat(32)}` as Hex, - bitcoinAddress: `0x${"05".repeat(25)}` as Hex, - litecoinAddress: `0x${"06".repeat(25)}` as Hex, -}; +/** + * Bytes constants used only for seeding devnet and testing. + */ +export const DEVNET_BYTES = { + abiBytes: `0x${"01".repeat(32)}`, + fourBytesInterface: "0x11100111", + publicKeyX: `0x${"02".repeat(32)}`, + publicKeyY: `0x${"03".repeat(32)}`, + contenthash: `0x${"04".repeat(32)}`, + bitcoinAddress: `0x${"05".repeat(25)}`, + litecoinAddress: `0x${"06".repeat(25)}`, +} as const satisfies Record; diff --git a/packages/integration-test-env/src/orchestrator.ts b/packages/integration-test-env/src/orchestrator.ts index c923f2ecf9..0bc2b04e71 100644 --- a/packages/integration-test-env/src/orchestrator.ts +++ b/packages/integration-test-env/src/orchestrator.ts @@ -255,12 +255,12 @@ async function main() { const devnetRpcUrl = `http://localhost:${devnetPort}`; log(`Devnet is ready (port ${devnetPort})`); - // Phase 1.5: Seed devnet with test data (before indexing starts) + // Phase 2: Seed devnet with test data (before indexing starts) log("Seeding devnet..."); await seedDevnet(devnetRpcUrl); log("Devnet seeded"); - // Phase 2: Download ENSRainbow database and start from source + // Phase 3: Download ENSRainbow database and start from source const DB_SCHEMA_VERSION = "3"; const LABEL_SET_ID = "ens-test-env"; const LABEL_SET_VERSION = "0"; @@ -307,7 +307,7 @@ async function main() { ); await waitForHealth(`http://localhost:${ENSRAINBOW_PORT}/health`, 30_000, "ENSRainbow"); - // Phase 3: Start ENSIndexer + // Phase 4: Start ENSIndexer log("Starting ENSIndexer..."); spawnService( "pnpm", @@ -326,10 +326,10 @@ async function main() { ); await waitForHealth(`http://localhost:${ENSINDEXER_PORT}/health`, 60_000, "ENSIndexer"); - // Phase 4: Wait for indexing to complete + // Phase 5: Wait for indexing to complete await pollIndexingStatus(ENSDB_URL, ENSINDEXER_SCHEMA_NAME, 30_000); - // Phase 5: Start ENSApi + // Phase 6: Start ENSApi log("Starting ENSApi..."); spawnService( "pnpm", @@ -343,7 +343,7 @@ async function main() { ); await waitForHealth(`http://localhost:${ENSAPI_PORT}/health`, 10_000, "ENSApi"); - // Phase 6: Run integration tests + // Phase 7: Run integration tests log("Running integration tests..."); execaSync("pnpm", ["test:integration", "--", "--bail", "1"], { cwd: MONOREPO_ROOT, diff --git a/packages/integration-test-env/src/seed/index.ts b/packages/integration-test-env/src/seed/index.ts index b9ddef4b5a..062bc4e488 100644 --- a/packages/integration-test-env/src/seed/index.ts +++ b/packages/integration-test-env/src/seed/index.ts @@ -1,46 +1,53 @@ import { type Account, type Chain, + createPublicClient, createWalletClient, http, + type PublicClient, + publicActions, type Transport, type WalletClient, } from "viem"; -import { mnemonicToAccount } from "viem/accounts"; import { ensTestEnvChain } from "@ensnode/datasources"; -import { DEVNET_MNEMONIC } from "@ensnode/ensnode-sdk/internal"; +import { DEVNET_ACCOUNTS } from "@ensnode/ensnode-sdk/internal"; import { seedPrimaryNameRecords } from "./primary-names"; import { seedResolverRecords } from "./resolver-records"; export type DevnetWalletClient = WalletClient; +export type DevnetReadClient = PublicClient; export type DevnetWalletClients = { - deployer: DevnetWalletClient; // index 0 — has REGISTRAR role on ETHRegistry - owner: DevnetWalletClient; // index 1 — DEVNET_OWNER, owns test.eth - user: DevnetWalletClient; // index 2 — DEVNET_USER - user2: DevnetWalletClient; // index 3 — DEVNET_USER2 + deployer: DevnetWalletClient; // index 0 + owner: DevnetWalletClient; // index 1 + user: DevnetWalletClient; // index 2 + user2: DevnetWalletClient; // index 3 }; function createDevnetWalletClients(rpcUrl: string): DevnetWalletClients { const transport = http(rpcUrl); - const mkClient = (addressIndex: number) => + const makeClient = (account: Account) => createWalletClient({ chain: ensTestEnvChain, transport, - account: mnemonicToAccount(DEVNET_MNEMONIC, { addressIndex }), - }); + account, + }).extend(publicActions); return { - deployer: mkClient(0), - owner: mkClient(1), - user: mkClient(2), - user2: mkClient(3), + deployer: makeClient(DEVNET_ACCOUNTS.deployer), + owner: makeClient(DEVNET_ACCOUNTS.owner), + user: makeClient(DEVNET_ACCOUNTS.user), + user2: makeClient(DEVNET_ACCOUNTS.user2), }; } export async function seedDevnet(rpcUrl: string): Promise { + const readClient = createPublicClient({ + chain: ensTestEnvChain, + transport: http(rpcUrl), + }); const clients = createDevnetWalletClients(rpcUrl); await seedPrimaryNameRecords(clients); - await seedResolverRecords(clients); + await seedResolverRecords(clients, readClient); } diff --git a/packages/integration-test-env/src/seed/resolver-records.ts b/packages/integration-test-env/src/seed/resolver-records.ts index b8823158ed..1a85ab7d6e 100644 --- a/packages/integration-test-env/src/seed/resolver-records.ts +++ b/packages/integration-test-env/src/seed/resolver-records.ts @@ -1,65 +1,83 @@ -import { type Address, createPublicClient, type Hex, http, namehash, toHex } from "viem"; +import { type Address, type Hex, namehash, toHex } from "viem"; import { packetToBytes } from "viem/ens"; -import { ensTestEnvChain } from "@ensnode/datasources"; -import { DEVNET_ACCOUNTS, DEVNET_BYTES, DEVNET_CONTRACTS } from "@ensnode/ensnode-sdk/internal"; +import { DEVNET_ADDRESSES, DEVNET_BYTES, DEVNET_CONTRACTS } from "@ensnode/ensnode-sdk/internal"; import { publicResolverAbi, universalResolverV2Abi } from "./abi"; -import type { DevnetWalletClient, DevnetWalletClients } from "./index"; +import type { DevnetReadClient, DevnetWalletClient, DevnetWalletClients } from "./index"; -const RESOLVER = DEVNET_CONTRACTS.permissionedResolver; +export async function seedResolverRecords( + clients: DevnetWalletClients, + readClient: DevnetReadClient, +): Promise { + await seedResolverRecordsForName( + clients, + readClient, + "test.eth", + DEVNET_CONTRACTS.permissionedResolver, + ); +} -export async function seedResolverRecords(clients: DevnetWalletClients): Promise { - const node = namehash("test.eth"); - await assertTestEthResolver(clients.owner.transport.url); +async function seedResolverRecordsForName( + clients: DevnetWalletClients, + readClient: DevnetReadClient, + name: string, + resolver: Address, +): Promise { + const node = namehash(name); + const actualResolver = await findResolver(readClient, name); + if (actualResolver.toLowerCase() !== resolver.toLowerCase()) { + throw new Error( + `${name} resolver mismatch: active=${actualResolver}, expected=${resolver}. Either resolver has been changed or something else is wrong.`, + ); + } // Text records - await setTextRecord(clients.owner, node, "avatar", "https://example.com/avatar.png"); - await setTextRecord(clients.owner, node, "com.twitter", "ensdomains"); - await setTextRecord(clients.owner, node, "com.github", "ensdomains"); - await setTextRecord(clients.owner, node, "url", "https://ens.domains"); - await setTextRecord(clients.owner, node, "email", "test@ens.domains"); - await setTextRecord(clients.owner, node, "description", "test.eth"); + await setTextRecord(clients.owner, resolver, node, "avatar", "https://example.com/avatar.png"); + await setTextRecord(clients.owner, resolver, node, "com.twitter", "ensdomains"); + await setTextRecord(clients.owner, resolver, node, "com.github", "ensdomains"); + await setTextRecord(clients.owner, resolver, node, "url", "https://ens.domains"); + await setTextRecord(clients.owner, resolver, node, "email", "test@ens.domains"); + await setTextRecord(clients.owner, resolver, node, "description", "test.eth"); // Multi-coin addresses // Coin 0 = Bitcoin - await setMulticoinAddress(clients.owner, node, 0n, DEVNET_BYTES.bitcoinAddress); + await setMulticoinAddress(clients.owner, resolver, node, 0n, DEVNET_BYTES.bitcoinAddress); // Coin 2 = Litecoin - await setMulticoinAddress(clients.owner, node, 2n, DEVNET_BYTES.litecoinAddress); + await setMulticoinAddress(clients.owner, resolver, node, 2n, DEVNET_BYTES.litecoinAddress); // Scalar resolver records - await setContenthash(clients.owner, node, DEVNET_BYTES.contenthash); - await setPubkey(clients.owner, node, DEVNET_BYTES.publicKeyX, DEVNET_BYTES.publicKeyY); - await setAbi(clients.owner, node, 1n, DEVNET_BYTES.abiBytes); + await setContenthash(clients.owner, resolver, node, DEVNET_BYTES.contenthash); + await setPubkey(clients.owner, resolver, node, DEVNET_BYTES.publicKeyX, DEVNET_BYTES.publicKeyY); + await setAbi(clients.owner, resolver, node, 1n, DEVNET_BYTES.abiBytes); await setInterfaceImplementer( clients.owner, + resolver, node, DEVNET_BYTES.fourBytesInterface, - DEVNET_ACCOUNTS.one, + DEVNET_ADDRESSES.one, ); } -async function assertTestEthResolver(rpcUrl: string): Promise { - const publicClient = createPublicClient({ chain: ensTestEnvChain, transport: http(rpcUrl) }); - const [activeResolver] = await publicClient.readContract({ +async function findResolver(readClient: DevnetReadClient, name: string): Promise
{ + const [resolver] = await readClient.readContract({ address: DEVNET_CONTRACTS.universalResolverV2, abi: universalResolverV2Abi, functionName: "findResolver", - args: [toHex(packetToBytes("test.eth"))], + args: [toHex(packetToBytes(name))], }); - if (activeResolver.toLowerCase() !== RESOLVER.toLowerCase()) { - throw new Error(`test.eth resolver mismatch: active=${activeResolver}, expected=${RESOLVER}`); - } + return resolver; } async function setTextRecord( walletClient: DevnetWalletClient, + resolver: Address, node: Hex, key: string, value: string, ): Promise { const hash = await walletClient.writeContract({ - address: RESOLVER, + address: resolver, abi: publicResolverAbi, functionName: "setText", args: [node, key, value], @@ -69,12 +87,13 @@ async function setTextRecord( async function setMulticoinAddress( walletClient: DevnetWalletClient, + resolver: Address, node: Hex, coinType: bigint, addressBytes: Hex, ): Promise { const hash = await walletClient.writeContract({ - address: RESOLVER, + address: resolver, abi: publicResolverAbi, functionName: "setAddr", args: [node, coinType, addressBytes], @@ -84,11 +103,12 @@ async function setMulticoinAddress( async function setContenthash( walletClient: DevnetWalletClient, + resolver: Address, node: Hex, hashValue: Hex, ): Promise { const hash = await walletClient.writeContract({ - address: RESOLVER, + address: resolver, abi: publicResolverAbi, functionName: "setContenthash", args: [node, hashValue], @@ -98,12 +118,13 @@ async function setContenthash( async function setPubkey( walletClient: DevnetWalletClient, + resolver: Address, node: Hex, x: Hex, y: Hex, ): Promise { const hash = await walletClient.writeContract({ - address: RESOLVER, + address: resolver, abi: publicResolverAbi, functionName: "setPubkey", args: [node, x, y], @@ -113,12 +134,13 @@ async function setPubkey( async function setAbi( walletClient: DevnetWalletClient, + resolver: Address, node: Hex, contentType: bigint, data: Hex, ): Promise { const hash = await walletClient.writeContract({ - address: RESOLVER, + address: resolver, abi: publicResolverAbi, functionName: "setABI", args: [node, contentType, data], @@ -128,12 +150,13 @@ async function setAbi( async function setInterfaceImplementer( walletClient: DevnetWalletClient, + resolver: Address, node: Hex, interfaceId: Hex, implementer: Address, ): Promise { const hash = await walletClient.writeContract({ - address: RESOLVER, + address: resolver, abi: publicResolverAbi, functionName: "setInterface", args: [node, interfaceId, implementer], From 4e512a737b3abf24eb4c29d29f6dddd70ae20db7 Mon Sep 17 00:00:00 2001 From: sevenzing Date: Sat, 25 Apr 2026 20:33:46 +0400 Subject: [PATCH 05/18] bump devnet version --- docker/services/devnet.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker/services/devnet.yml b/docker/services/devnet.yml index ba16e19b42..5d5fa27a7d 100644 --- a/docker/services/devnet.yml +++ b/docker/services/devnet.yml @@ -1,7 +1,7 @@ services: devnet: container_name: devnet - image: ghcr.io/ensdomains/contracts-v2:main-e8696c6 + image: ghcr.io/ensdomains/contracts-v2:main-9f26a8f command: ./script/runDevnet.ts --testNames pull_policy: always ports: From 51239ad6c74116749cdc76db02a58f88d7cc690b Mon Sep 17 00:00:00 2001 From: sevenzing Date: Sat, 25 Apr 2026 20:35:06 +0400 Subject: [PATCH 06/18] fix tests --- packages/ensnode-sdk/src/shared/devnet/addresses.ts | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/packages/ensnode-sdk/src/shared/devnet/addresses.ts b/packages/ensnode-sdk/src/shared/devnet/addresses.ts index 3349e31c53..11c003eb39 100644 --- a/packages/ensnode-sdk/src/shared/devnet/addresses.ts +++ b/packages/ensnode-sdk/src/shared/devnet/addresses.ts @@ -108,11 +108,16 @@ const DEVNET_MNEMONIC = "test test test test test test test test test test test * Named signer accounts from the ens-test-env devnet. * Derived from the standard Hardhat mnemonic at account indices 0-3. */ +function deriveNormalizedAccount(addressIndex: number) { + const account = mnemonicToAccount(DEVNET_MNEMONIC, { addressIndex }); + return { ...account, address: toNormalizedAddress(account.address) }; +} + export const DEVNET_ACCOUNTS = { - deployer: mnemonicToAccount(DEVNET_MNEMONIC, { addressIndex: 0 }), - owner: mnemonicToAccount(DEVNET_MNEMONIC, { addressIndex: 1 }), - user: mnemonicToAccount(DEVNET_MNEMONIC, { addressIndex: 2 }), - user2: mnemonicToAccount(DEVNET_MNEMONIC, { addressIndex: 3 }), + deployer: deriveNormalizedAccount(0), + owner: deriveNormalizedAccount(1), + user: deriveNormalizedAccount(2), + user2: deriveNormalizedAccount(3), } as const; /** From dbee41e0d06e9972e6ee8e2f34440b2058fc7891 Mon Sep 17 00:00:00 2001 From: sevenzing Date: Sat, 25 Apr 2026 20:51:35 +0400 Subject: [PATCH 07/18] wait for transactions to complete --- .../integration-test-env/src/seed/index.ts | 28 ++++++------ .../src/seed/primary-names.ts | 6 ++- .../src/seed/resolver-records.ts | 45 +++++++++++++------ 3 files changed, 50 insertions(+), 29 deletions(-) diff --git a/packages/integration-test-env/src/seed/index.ts b/packages/integration-test-env/src/seed/index.ts index 062bc4e488..81f940f200 100644 --- a/packages/integration-test-env/src/seed/index.ts +++ b/packages/integration-test-env/src/seed/index.ts @@ -1,11 +1,10 @@ import { type Account, type Chain, - createPublicClient, createWalletClient, http, - type PublicClient, publicActions, + type PublicActions, type Transport, type WalletClient, } from "viem"; @@ -16,8 +15,15 @@ import { DEVNET_ACCOUNTS } from "@ensnode/ensnode-sdk/internal"; import { seedPrimaryNameRecords } from "./primary-names"; import { seedResolverRecords } from "./resolver-records"; -export type DevnetWalletClient = WalletClient; -export type DevnetReadClient = PublicClient; +function createDevnetWalletClient(transport: Transport, account: Account) { + return createWalletClient({ + chain: ensTestEnvChain, + transport, + account, + }).extend(publicActions); +} + +export type DevnetWalletClient = WalletClient & PublicActions; export type DevnetWalletClients = { deployer: DevnetWalletClient; // index 0 @@ -28,12 +34,8 @@ export type DevnetWalletClients = { function createDevnetWalletClients(rpcUrl: string): DevnetWalletClients { const transport = http(rpcUrl); - const makeClient = (account: Account) => - createWalletClient({ - chain: ensTestEnvChain, - transport, - account, - }).extend(publicActions); + const makeClient = (account: Account): DevnetWalletClient => + createDevnetWalletClient(transport, account); return { deployer: makeClient(DEVNET_ACCOUNTS.deployer), owner: makeClient(DEVNET_ACCOUNTS.owner), @@ -43,11 +45,7 @@ function createDevnetWalletClients(rpcUrl: string): DevnetWalletClients { } export async function seedDevnet(rpcUrl: string): Promise { - const readClient = createPublicClient({ - chain: ensTestEnvChain, - transport: http(rpcUrl), - }); const clients = createDevnetWalletClients(rpcUrl); await seedPrimaryNameRecords(clients); - await seedResolverRecords(clients, readClient); + await seedResolverRecords(clients); } diff --git a/packages/integration-test-env/src/seed/primary-names.ts b/packages/integration-test-env/src/seed/primary-names.ts index 8621bc6c0d..aab5a9e6a7 100644 --- a/packages/integration-test-env/src/seed/primary-names.ts +++ b/packages/integration-test-env/src/seed/primary-names.ts @@ -7,12 +7,16 @@ export async function seedPrimaryNameRecords(clients: DevnetWalletClients): Prom await setPrimaryNameRecord(clients.owner, "test.eth"); } -async function setPrimaryNameRecord(walletClient: DevnetWalletClient, name: string): Promise { +async function setPrimaryNameRecord( + walletClient: DevnetWalletClient, + name: string, +): Promise { const hash = await walletClient.writeContract({ address: DEVNET_CONTRACTS.ethReverseRegistrar, abi: ethReverseRegistrarAbi, functionName: "setName", args: [name], }); + await walletClient.waitForTransactionReceipt({ hash }); console.log(`[seed] setPrimaryNameRecord("${name}") tx: ${hash}`); } diff --git a/packages/integration-test-env/src/seed/resolver-records.ts b/packages/integration-test-env/src/seed/resolver-records.ts index 1a85ab7d6e..9b43f3ba8f 100644 --- a/packages/integration-test-env/src/seed/resolver-records.ts +++ b/packages/integration-test-env/src/seed/resolver-records.ts @@ -4,15 +4,11 @@ import { packetToBytes } from "viem/ens"; import { DEVNET_ADDRESSES, DEVNET_BYTES, DEVNET_CONTRACTS } from "@ensnode/ensnode-sdk/internal"; import { publicResolverAbi, universalResolverV2Abi } from "./abi"; -import type { DevnetReadClient, DevnetWalletClient, DevnetWalletClients } from "./index"; +import type { DevnetWalletClient, DevnetWalletClients } from "./index"; -export async function seedResolverRecords( - clients: DevnetWalletClients, - readClient: DevnetReadClient, -): Promise { +export async function seedResolverRecords(clients: DevnetWalletClients): Promise { await seedResolverRecordsForName( clients, - readClient, "test.eth", DEVNET_CONTRACTS.permissionedResolver, ); @@ -20,12 +16,11 @@ export async function seedResolverRecords( async function seedResolverRecordsForName( clients: DevnetWalletClients, - readClient: DevnetReadClient, name: string, resolver: Address, ): Promise { const node = namehash(name); - const actualResolver = await findResolver(readClient, name); + const actualResolver = await findResolver(clients.owner, name); if (actualResolver.toLowerCase() !== resolver.toLowerCase()) { throw new Error( `${name} resolver mismatch: active=${actualResolver}, expected=${resolver}. Either resolver has been changed or something else is wrong.`, @@ -42,13 +37,31 @@ async function seedResolverRecordsForName( // Multi-coin addresses // Coin 0 = Bitcoin - await setMulticoinAddress(clients.owner, resolver, node, 0n, DEVNET_BYTES.bitcoinAddress); + await setMulticoinAddress( + clients.owner, + resolver, + node, + 0n, + DEVNET_BYTES.bitcoinAddress, + ); // Coin 2 = Litecoin - await setMulticoinAddress(clients.owner, resolver, node, 2n, DEVNET_BYTES.litecoinAddress); + await setMulticoinAddress( + clients.owner, + resolver, + node, + 2n, + DEVNET_BYTES.litecoinAddress, + ); // Scalar resolver records await setContenthash(clients.owner, resolver, node, DEVNET_BYTES.contenthash); - await setPubkey(clients.owner, resolver, node, DEVNET_BYTES.publicKeyX, DEVNET_BYTES.publicKeyY); + await setPubkey( + clients.owner, + resolver, + node, + DEVNET_BYTES.publicKeyX, + DEVNET_BYTES.publicKeyY, + ); await setAbi(clients.owner, resolver, node, 1n, DEVNET_BYTES.abiBytes); await setInterfaceImplementer( clients.owner, @@ -59,8 +72,8 @@ async function seedResolverRecordsForName( ); } -async function findResolver(readClient: DevnetReadClient, name: string): Promise
{ - const [resolver] = await readClient.readContract({ +async function findResolver(client: DevnetWalletClient, name: string): Promise
{ + const [resolver] = await client.readContract({ address: DEVNET_CONTRACTS.universalResolverV2, abi: universalResolverV2Abi, functionName: "findResolver", @@ -82,6 +95,7 @@ async function setTextRecord( functionName: "setText", args: [node, key, value], }); + await walletClient.waitForTransactionReceipt({ hash }); console.log(`[seed] setText("${key}", "${value}") tx: ${hash}`); } @@ -98,6 +112,7 @@ async function setMulticoinAddress( functionName: "setAddr", args: [node, coinType, addressBytes], }); + await walletClient.waitForTransactionReceipt({ hash }); console.log(`[seed] setAddr(coinType=${coinType}) tx: ${hash}`); } @@ -113,6 +128,7 @@ async function setContenthash( functionName: "setContenthash", args: [node, hashValue], }); + await walletClient.waitForTransactionReceipt({ hash }); console.log(`[seed] setContenthash() tx: ${hash}`); } @@ -129,6 +145,7 @@ async function setPubkey( functionName: "setPubkey", args: [node, x, y], }); + await walletClient.waitForTransactionReceipt({ hash }); console.log(`[seed] setPubkey() tx: ${hash}`); } @@ -145,6 +162,7 @@ async function setAbi( functionName: "setABI", args: [node, contentType, data], }); + await walletClient.waitForTransactionReceipt({ hash }); console.log(`[seed] setABI(contentType=${contentType}) tx: ${hash}`); } @@ -161,5 +179,6 @@ async function setInterfaceImplementer( functionName: "setInterface", args: [node, interfaceId, implementer], }); + await walletClient.waitForTransactionReceipt({ hash }); console.log(`[seed] setInterface(interfaceId=${interfaceId}) tx: ${hash}`); } From 9c8752f2c334e569b1d5ad842156e41ebc16d3c3 Mon Sep 17 00:00:00 2001 From: sevenzing Date: Sat, 25 Apr 2026 21:06:16 +0400 Subject: [PATCH 08/18] fix tests --- .../src/shared/config/rpc-configs-from-env.ts | 3 +- .../integration-test-env/src/seed/index.ts | 2 +- .../src/seed/primary-names.ts | 5 +--- .../src/seed/resolver-records.ts | 30 +++---------------- 4 files changed, 8 insertions(+), 32 deletions(-) diff --git a/packages/ensnode-sdk/src/shared/config/rpc-configs-from-env.ts b/packages/ensnode-sdk/src/shared/config/rpc-configs-from-env.ts index e4dd604229..70a17d0a95 100644 --- a/packages/ensnode-sdk/src/shared/config/rpc-configs-from-env.ts +++ b/packages/ensnode-sdk/src/shared/config/rpc-configs-from-env.ts @@ -3,6 +3,7 @@ import type { ChainIdString } from "enssdk"; import { type Datasource, type ENSNamespaceId, + ENSNamespaceIds, ensTestEnvChain, getENSNamespace, } from "@ensnode/datasources"; @@ -129,7 +130,7 @@ export function buildRpcConfigsFromEnv( } // ens-test-env Chain - if (chain.id === ensTestEnvChain.id) { + if (namespace === ENSNamespaceIds.EnsTestEnv && chain.id === ensTestEnvChain.id) { rpcConfigs[serializeChainId(ensTestEnvChain.id)] = ensTestEnvChain.rpcUrls.default.http[0]; continue; } diff --git a/packages/integration-test-env/src/seed/index.ts b/packages/integration-test-env/src/seed/index.ts index 81f940f200..19f36ca917 100644 --- a/packages/integration-test-env/src/seed/index.ts +++ b/packages/integration-test-env/src/seed/index.ts @@ -3,8 +3,8 @@ import { type Chain, createWalletClient, http, - publicActions, type PublicActions, + publicActions, type Transport, type WalletClient, } from "viem"; diff --git a/packages/integration-test-env/src/seed/primary-names.ts b/packages/integration-test-env/src/seed/primary-names.ts index aab5a9e6a7..9989a0cc92 100644 --- a/packages/integration-test-env/src/seed/primary-names.ts +++ b/packages/integration-test-env/src/seed/primary-names.ts @@ -7,10 +7,7 @@ export async function seedPrimaryNameRecords(clients: DevnetWalletClients): Prom await setPrimaryNameRecord(clients.owner, "test.eth"); } -async function setPrimaryNameRecord( - walletClient: DevnetWalletClient, - name: string, -): Promise { +async function setPrimaryNameRecord(walletClient: DevnetWalletClient, name: string): Promise { const hash = await walletClient.writeContract({ address: DEVNET_CONTRACTS.ethReverseRegistrar, abi: ethReverseRegistrarAbi, diff --git a/packages/integration-test-env/src/seed/resolver-records.ts b/packages/integration-test-env/src/seed/resolver-records.ts index 9b43f3ba8f..c178f418ba 100644 --- a/packages/integration-test-env/src/seed/resolver-records.ts +++ b/packages/integration-test-env/src/seed/resolver-records.ts @@ -7,11 +7,7 @@ import { publicResolverAbi, universalResolverV2Abi } from "./abi"; import type { DevnetWalletClient, DevnetWalletClients } from "./index"; export async function seedResolverRecords(clients: DevnetWalletClients): Promise { - await seedResolverRecordsForName( - clients, - "test.eth", - DEVNET_CONTRACTS.permissionedResolver, - ); + await seedResolverRecordsForName(clients, "test.eth", DEVNET_CONTRACTS.permissionedResolver); } async function seedResolverRecordsForName( @@ -37,31 +33,13 @@ async function seedResolverRecordsForName( // Multi-coin addresses // Coin 0 = Bitcoin - await setMulticoinAddress( - clients.owner, - resolver, - node, - 0n, - DEVNET_BYTES.bitcoinAddress, - ); + await setMulticoinAddress(clients.owner, resolver, node, 0n, DEVNET_BYTES.bitcoinAddress); // Coin 2 = Litecoin - await setMulticoinAddress( - clients.owner, - resolver, - node, - 2n, - DEVNET_BYTES.litecoinAddress, - ); + await setMulticoinAddress(clients.owner, resolver, node, 2n, DEVNET_BYTES.litecoinAddress); // Scalar resolver records await setContenthash(clients.owner, resolver, node, DEVNET_BYTES.contenthash); - await setPubkey( - clients.owner, - resolver, - node, - DEVNET_BYTES.publicKeyX, - DEVNET_BYTES.publicKeyY, - ); + await setPubkey(clients.owner, resolver, node, DEVNET_BYTES.publicKeyX, DEVNET_BYTES.publicKeyY); await setAbi(clients.owner, resolver, node, 1n, DEVNET_BYTES.abiBytes); await setInterfaceImplementer( clients.owner, From d985bf0bba3831335a9685bacf8862408ffda927 Mon Sep 17 00:00:00 2001 From: sevenzing Date: Sat, 25 Apr 2026 22:16:05 +0400 Subject: [PATCH 09/18] fix disabled cache --- apps/ensindexer/src/lib/ponder-helpers.ts | 8 +++++--- apps/ensindexer/src/plugins/tokenscope/plugin.ts | 12 ++++++------ packages/datasources/src/lib/chains.ts | 2 +- packages/ensnode-sdk/src/shared/devnet/addresses.ts | 8 ++++---- 4 files changed, 16 insertions(+), 14 deletions(-) diff --git a/apps/ensindexer/src/lib/ponder-helpers.ts b/apps/ensindexer/src/lib/ponder-helpers.ts index b38d40268c..f5904b3722 100644 --- a/apps/ensindexer/src/lib/ponder-helpers.ts +++ b/apps/ensindexer/src/lib/ponder-helpers.ts @@ -11,7 +11,7 @@ import { type ContractConfig, type DatasourceName, type ENSNamespaceId, - ensTestEnvChain, + ENSNamespaceIds, maybeGetDatasource, } from "@ensnode/datasources"; import { type BlockNumberRange, buildBlockNumberRange, type ChainId } from "@ensnode/ponder-sdk"; @@ -96,6 +96,7 @@ export const constrainBlockrange = ( export function chainsConnectionConfig( rpcConfigs: ENSIndexerConfig["rpcConfigs"], chainId: ChainId, + namespace?: ENSNamespaceId, ) { const rpcConfig = rpcConfigs.get(chainId); @@ -106,7 +107,8 @@ export function chainsConnectionConfig( } // NOTE: disable cache on local chains (e.g. ganache, anvil, ens-test-env) - const disableCache = chainId === 31337 || chainId === 1337 || chainId === ensTestEnvChain.id; + const disableCache = + chainId === 31337 || chainId === 1337 || namespace === ENSNamespaceIds.EnsTestEnv; return { [chainId.toString()]: { @@ -165,7 +167,7 @@ export function chainsConnectionConfigForDatasources( .reduce>( (memo, chain) => ({ ...memo, - ...chainsConnectionConfig(rpcConfigs, chain.id), + ...chainsConnectionConfig(rpcConfigs, chain.id, namespace), }), {}, ); diff --git a/apps/ensindexer/src/plugins/tokenscope/plugin.ts b/apps/ensindexer/src/plugins/tokenscope/plugin.ts index e844ca10e7..56708e3deb 100644 --- a/apps/ensindexer/src/plugins/tokenscope/plugin.ts +++ b/apps/ensindexer/src/plugins/tokenscope/plugin.ts @@ -55,12 +55,12 @@ export default createPlugin({ return ponder.createConfig({ chains: { - ...chainsConnectionConfig(config.rpcConfigs, seaport.chain.id), - ...chainsConnectionConfig(config.rpcConfigs, ensroot.chain.id), - ...chainsConnectionConfig(config.rpcConfigs, basenames.chain.id), - ...chainsConnectionConfig(config.rpcConfigs, lineanames.chain.id), - ...chainsConnectionConfig(config.rpcConfigs, threednsOptimism.chain.id), - ...chainsConnectionConfig(config.rpcConfigs, threednsBase.chain.id), + ...chainsConnectionConfig(config.rpcConfigs, seaport.chain.id, config.namespace), + ...chainsConnectionConfig(config.rpcConfigs, ensroot.chain.id, config.namespace), + ...chainsConnectionConfig(config.rpcConfigs, basenames.chain.id, config.namespace), + ...chainsConnectionConfig(config.rpcConfigs, lineanames.chain.id, config.namespace), + ...chainsConnectionConfig(config.rpcConfigs, threednsOptimism.chain.id, config.namespace), + ...chainsConnectionConfig(config.rpcConfigs, threednsBase.chain.id, config.namespace), }, contracts: { [namespaceContract(pluginName, "Seaport")]: { diff --git a/packages/datasources/src/lib/chains.ts b/packages/datasources/src/lib/chains.ts index cc019cc31c..cb4e5c2cc7 100644 --- a/packages/datasources/src/lib/chains.ts +++ b/packages/datasources/src/lib/chains.ts @@ -2,7 +2,7 @@ import { type Chain, localhost } from "viem/chains"; /** * The ens-test-env chain id is 1: - * @see https://github.com/ensdomains/contracts-v2/blob/762de44d60b2588b2e92a6d29df941c4de821ae6/contracts/script/setup.ts#L40 + * @see https://github.com/ensdomains/contracts-v2/blob/9f26a8f01f1f87db1c5d57b9faa8e76f0c5043ef/contracts/script/setup.ts#L91 */ export const ensTestEnvChain = { diff --git a/packages/ensnode-sdk/src/shared/devnet/addresses.ts b/packages/ensnode-sdk/src/shared/devnet/addresses.ts index 11c003eb39..a06421eccf 100644 --- a/packages/ensnode-sdk/src/shared/devnet/addresses.ts +++ b/packages/ensnode-sdk/src/shared/devnet/addresses.ts @@ -104,15 +104,15 @@ export const DEVNET_CONTRACTS = { */ const DEVNET_MNEMONIC = "test test test test test test test test test test test junk"; -/** - * Named signer accounts from the ens-test-env devnet. - * Derived from the standard Hardhat mnemonic at account indices 0-3. - */ function deriveNormalizedAccount(addressIndex: number) { const account = mnemonicToAccount(DEVNET_MNEMONIC, { addressIndex }); return { ...account, address: toNormalizedAddress(account.address) }; } +/** + * Named signer accounts from the ens-test-env devnet. + * Derived from the standard Hardhat mnemonic at account indices 0-3. + */ export const DEVNET_ACCOUNTS = { deployer: deriveNormalizedAccount(0), owner: deriveNormalizedAccount(1), From 224ae50c1e34fb097273c5538df610decc62e4ea Mon Sep 17 00:00:00 2001 From: sevenzing Date: Sat, 25 Apr 2026 22:45:55 +0400 Subject: [PATCH 10/18] add @datasources/devnet --- .../resolve-primary-name.integration.test.ts | 10 +- .../resolve-primary-names.integration.test.ts | 10 +- .../resolve-records.integration.test.ts | 40 +++--- .../schema/account.integration.test.ts | 32 ++--- packages/datasources/package.json | 13 +- packages/datasources/src/devnet/constants.ts | 131 ++++++++++++++++++ packages/datasources/src/devnet/index.ts | 1 + .../{ens-test-env.ts => devnet/namespace.ts} | 65 ++++----- packages/datasources/src/namespaces.ts | 2 +- packages/datasources/tsup.config.ts | 2 +- packages/ensnode-sdk/src/internal.ts | 2 - .../src/omnigraph-api/example-queries.ts | 10 +- .../src/shared/devnet/addresses.ts | 129 ----------------- .../ensnode-sdk/src/shared/devnet/data.ts | 14 -- .../integration-test-env/src/seed/index.ts | 10 +- .../src/seed/primary-names.ts | 4 +- .../src/seed/resolver-records.ts | 20 +-- 17 files changed, 245 insertions(+), 250 deletions(-) create mode 100644 packages/datasources/src/devnet/constants.ts create mode 100644 packages/datasources/src/devnet/index.ts rename packages/datasources/src/{ens-test-env.ts => devnet/namespace.ts} (64%) delete mode 100644 packages/ensnode-sdk/src/shared/devnet/addresses.ts delete mode 100644 packages/ensnode-sdk/src/shared/devnet/data.ts diff --git a/apps/ensapi/src/handlers/api/resolution/resolve-primary-name.integration.test.ts b/apps/ensapi/src/handlers/api/resolution/resolve-primary-name.integration.test.ts index d5597ce660..7f928e1609 100644 --- a/apps/ensapi/src/handlers/api/resolution/resolve-primary-name.integration.test.ts +++ b/apps/ensapi/src/handlers/api/resolution/resolve-primary-name.integration.test.ts @@ -6,7 +6,7 @@ import { describe, expect, it } from "vitest"; -import { DEVNET_ACCOUNTS } from "@ensnode/ensnode-sdk/internal"; +import { accounts } from "@ensnode/datasources/devnet"; const BASE_URL = process.env.ENSNODE_URL!; @@ -14,7 +14,7 @@ describe("GET /api/resolve/primary-name/:address/:chainId", () => { it.each([ { description: "resolves primary name for owner address on chain 1", - address: DEVNET_ACCOUNTS.owner.address, + address: accounts.owner.address, chainId: "1", query: "", expectedStatus: 200, @@ -26,7 +26,7 @@ describe("GET /api/resolve/primary-name/:address/:chainId", () => { }, { description: "returns null for user without a primary name", - address: DEVNET_ACCOUNTS.user.address, + address: accounts.user.address, chainId: "1", query: "", expectedStatus: 200, @@ -34,7 +34,7 @@ describe("GET /api/resolve/primary-name/:address/:chainId", () => { }, { description: "owner address with accelerate=true returns accelerationRequested: true", - address: DEVNET_ACCOUNTS.owner.address, + address: accounts.owner.address, chainId: "1", query: "accelerate=true", expectedStatus: 200, @@ -60,7 +60,7 @@ describe("GET /api/resolve/primary-name/:address/:chainId", () => { }, { description: "returns 400 for non-numeric chainId", - address: DEVNET_ACCOUNTS.owner.address, + address: accounts.owner.address, chainId: "notachainid", query: "", expectedStatus: 400, diff --git a/apps/ensapi/src/handlers/api/resolution/resolve-primary-names.integration.test.ts b/apps/ensapi/src/handlers/api/resolution/resolve-primary-names.integration.test.ts index 6fdcd204e8..6625025f4f 100644 --- a/apps/ensapi/src/handlers/api/resolution/resolve-primary-names.integration.test.ts +++ b/apps/ensapi/src/handlers/api/resolution/resolve-primary-names.integration.test.ts @@ -6,7 +6,7 @@ import { describe, expect, it } from "vitest"; -import { DEVNET_ACCOUNTS } from "@ensnode/ensnode-sdk/internal"; +import { accounts } from "@ensnode/datasources/devnet"; const BASE_URL = process.env.ENSNODE_URL!; @@ -14,7 +14,7 @@ describe("GET /api/resolve/primary-names/:address", () => { it.each([ { description: "resolves primary names for owner address on chain 1", - address: DEVNET_ACCOUNTS.owner.address, + address: accounts.owner.address, query: "chainIds=1", expectedStatus: 200, expectedBody: { @@ -25,7 +25,7 @@ describe("GET /api/resolve/primary-names/:address", () => { }, { description: "resolves all primary names", - address: DEVNET_ACCOUNTS.owner.address, + address: accounts.owner.address, query: "", expectedStatus: 200, expectedBody: { @@ -53,7 +53,7 @@ describe("GET /api/resolve/primary-names/:address", () => { }, { description: "returns 400 when chainIds contains the default chain id (0)", - address: DEVNET_ACCOUNTS.owner.address, + address: accounts.owner.address, query: "chainIds=0", expectedStatus: 400, expectedBody: { @@ -75,7 +75,7 @@ describe("GET /api/resolve/primary-names/:address", () => { }, { description: "returns 400 when chainIds contains duplicate chain ids", - address: DEVNET_ACCOUNTS.owner.address, + address: accounts.owner.address, query: "chainIds=1,1", expectedStatus: 400, expectedBody: { diff --git a/apps/ensapi/src/handlers/api/resolution/resolve-records.integration.test.ts b/apps/ensapi/src/handlers/api/resolution/resolve-records.integration.test.ts index 47f00cbf61..a908cfe091 100644 --- a/apps/ensapi/src/handlers/api/resolution/resolve-records.integration.test.ts +++ b/apps/ensapi/src/handlers/api/resolution/resolve-records.integration.test.ts @@ -6,7 +6,7 @@ import { describe, expect, it } from "vitest"; -import { DEVNET_ACCOUNTS, DEVNET_ADDRESSES, DEVNET_BYTES } from "@ensnode/ensnode-sdk/internal"; +import { accounts, addresses, fixtures } from "@ensnode/datasources/devnet"; const BASE_URL = process.env.ENSNODE_URL!; @@ -18,7 +18,7 @@ describe("GET /api/resolve/records/:name", () => { query: "addresses=60", expectedStatus: 200, expectedBody: { - records: { addresses: { 60: DEVNET_ACCOUNTS.owner.address } }, + records: { addresses: { 60: accounts.owner.address } }, accelerationRequested: false, accelerationAttempted: false, }, @@ -30,7 +30,7 @@ describe("GET /api/resolve/records/:name", () => { query: "addresses=60", expectedStatus: 200, expectedBody: { - records: { addresses: { 60: DEVNET_ACCOUNTS.owner.address } }, + records: { addresses: { 60: accounts.owner.address } }, accelerationRequested: false, accelerationAttempted: false, }, @@ -65,7 +65,7 @@ describe("GET /api/resolve/records/:name", () => { expectedStatus: 200, expectedBody: { records: { - addresses: { 60: DEVNET_ACCOUNTS.owner.address }, + addresses: { 60: accounts.owner.address }, texts: { description: "example.eth" }, }, accelerationRequested: false, @@ -90,7 +90,7 @@ describe("GET /api/resolve/records/:name", () => { query: "addresses=60", expectedStatus: 200, expectedBody: { - records: { addresses: { 60: DEVNET_ACCOUNTS.owner.address } }, + records: { addresses: { 60: accounts.owner.address } }, accelerationRequested: false, accelerationAttempted: false, }, @@ -112,7 +112,7 @@ describe("GET /api/resolve/records/:name", () => { query: "addresses=60", expectedStatus: 200, expectedBody: { - records: { addresses: { 60: DEVNET_ACCOUNTS.owner.address } }, + records: { addresses: { 60: accounts.owner.address } }, accelerationRequested: false, accelerationAttempted: false, }, @@ -124,7 +124,7 @@ describe("GET /api/resolve/records/:name", () => { query: "addresses=60", expectedStatus: 200, expectedBody: { - records: { addresses: { 60: DEVNET_ACCOUNTS.owner.address } }, + records: { addresses: { 60: accounts.owner.address } }, accelerationRequested: false, accelerationAttempted: false, }, @@ -161,9 +161,9 @@ describe("GET /api/resolve/records/:name", () => { expectedBody: { records: { addresses: { - 60: DEVNET_ACCOUNTS.owner.address, - 0: DEVNET_BYTES.bitcoinAddress, - 2: DEVNET_BYTES.litecoinAddress, + 60: accounts.owner.address, + 0: fixtures.bitcoinAddress, + 2: fixtures.litecoinAddress, 777777: null, }, }, @@ -183,15 +183,15 @@ describe("GET /api/resolve/records/:name", () => { "pubkey=true", "version=true", "abi=1", - `interfaces=${DEVNET_BYTES.fourBytesInterface}`, + `interfaces=${fixtures.fourBytesInterface}`, ].join("&"), expectedStatus: 200, expectedBody: { records: { addresses: { - 60: DEVNET_ACCOUNTS.owner.address, - 0: DEVNET_BYTES.bitcoinAddress, - 2: DEVNET_BYTES.litecoinAddress, + 60: accounts.owner.address, + 0: fixtures.bitcoinAddress, + 2: fixtures.litecoinAddress, }, texts: { avatar: "https://example.com/avatar.png", @@ -201,18 +201,18 @@ describe("GET /api/resolve/records/:name", () => { "com.twitter": "ensdomains", "com.github": "ensdomains", }, - contenthash: DEVNET_BYTES.contenthash, + contenthash: fixtures.contenthash, pubkey: { - x: DEVNET_BYTES.publicKeyX, - y: DEVNET_BYTES.publicKeyY, + x: fixtures.publicKeyX, + y: fixtures.publicKeyY, }, version: expect.any(String), abi: { contentType: "1", - data: DEVNET_BYTES.abiBytes, + data: fixtures.abiBytes, }, interfaces: { - [DEVNET_BYTES.fourBytesInterface]: DEVNET_ADDRESSES.one, + [fixtures.fourBytesInterface]: addresses.one, }, }, accelerationRequested: false, @@ -226,7 +226,7 @@ describe("GET /api/resolve/records/:name", () => { query: "addresses=60&accelerate=true", expectedStatus: 200, expectedBody: { - records: { addresses: { 60: DEVNET_ACCOUNTS.owner.address } }, + records: { addresses: { 60: accounts.owner.address } }, accelerationRequested: true, accelerationAttempted: false, }, 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 8dfe525c48..9b5ce6975f 100644 --- a/apps/ensapi/src/omnigraph-api/schema/account.integration.test.ts +++ b/apps/ensapi/src/omnigraph-api/schema/account.integration.test.ts @@ -1,7 +1,7 @@ import type { InterpretedName } from "enssdk"; import { beforeAll, describe, expect, it } from "vitest"; -import { DEVNET_ACCOUNTS } from "@ensnode/ensnode-sdk/internal"; +import { accounts } from "@ensnode/datasources/devnet"; import { AccountDomainsPaginated, @@ -37,7 +37,7 @@ describe("Account.domains", () => { it("returns domains owned by the devnet owner", async () => { const result = await request(AccountDomains, { - address: DEVNET_ACCOUNTS.owner.address, + address: accounts.owner.address, }); const domains = flattenConnection(result.account.domains); const names = domains.map((d) => d.name); @@ -64,7 +64,7 @@ describe("Account.domains", () => { it("returns domains owned by the new owner", async () => { const result = await request(AccountDomains, { - address: DEVNET_ACCOUNTS.user.address, + address: accounts.user.address, }); const domains = flattenConnection(result.account.domains); const names = domains.map((d) => d.name); @@ -77,7 +77,7 @@ describe("Account.domains pagination", () => { testDomainPagination(async (variables) => { const result = await request<{ account: { domains: PaginatedGraphQLConnection }; - }>(AccountDomainsPaginated, { address: DEVNET_ACCOUNTS.owner.address, ...variables }); + }>(AccountDomainsPaginated, { address: accounts.owner.address, ...variables }); return result.account.domains; }); }); @@ -94,14 +94,14 @@ describe("Account.events", () => { it("returns events for the devnet deployer", async () => { const result = await request(AccountEvents, { - address: DEVNET_ACCOUNTS.deployer.address, + address: accounts.deployer.address, }); const events = flattenConnection(result.account.events); expect(events.length).toBeGreaterThan(0); for (const event of events) { - expect(event.from).toBe(DEVNET_ACCOUNTS.deployer.address); + expect(event.from).toBe(accounts.deployer.address); } }); }); @@ -110,7 +110,7 @@ describe("Account.events pagination", () => { testEventPagination(async (variables) => { const result = await request<{ account: { events: PaginatedGraphQLConnection }; - }>(AccountEventsPaginated, { address: DEVNET_ACCOUNTS.deployer.address, ...variables }); + }>(AccountEventsPaginated, { address: accounts.deployer.address, ...variables }); return result.account.events; }); }); @@ -129,7 +129,7 @@ describe("Account.events filtering (AccountEventsWhereInput)", () => { beforeAll(async () => { const result = await request(AccountEventsFiltered, { - address: DEVNET_ACCOUNTS.deployer.address, + address: accounts.deployer.address, first: 1000, }); // events are returned in ascending order, so first/last access yields min/max values @@ -141,7 +141,7 @@ describe("Account.events filtering (AccountEventsWhereInput)", () => { const targetSelector = allEvents[0].topics[0]; const result = await request(AccountEventsFiltered, { - address: DEVNET_ACCOUNTS.deployer.address, + address: accounts.deployer.address, where: { selector_in: [targetSelector] }, }); const events = flattenConnection(result.account.events); @@ -154,7 +154,7 @@ describe("Account.events filtering (AccountEventsWhereInput)", () => { it("filters by selector_in with unknown topic returns no results", async () => { const result = await request(AccountEventsFiltered, { - address: DEVNET_ACCOUNTS.deployer.address, + address: accounts.deployer.address, where: { selector_in: ["0x0000000000000000000000000000000000000000000000000000000000000001"], }, @@ -165,7 +165,7 @@ describe("Account.events filtering (AccountEventsWhereInput)", () => { it("filters by empty selector_in returns no results", async () => { const result = await request(AccountEventsFiltered, { - address: DEVNET_ACCOUNTS.deployer.address, + address: accounts.deployer.address, where: { selector_in: [] }, }); const events = flattenConnection(result.account.events); @@ -176,7 +176,7 @@ describe("Account.events filtering (AccountEventsWhereInput)", () => { const midTimestamp = allEvents[Math.floor(allEvents.length / 2)].timestamp; const result = await request(AccountEventsFiltered, { - address: DEVNET_ACCOUNTS.deployer.address, + address: accounts.deployer.address, where: { timestamp_gte: midTimestamp }, }); const events = flattenConnection(result.account.events); @@ -192,7 +192,7 @@ describe("Account.events filtering (AccountEventsWhereInput)", () => { const midTimestamp = allEvents[Math.floor(allEvents.length / 2)].timestamp; const result = await request(AccountEventsFiltered, { - address: DEVNET_ACCOUNTS.deployer.address, + address: accounts.deployer.address, where: { timestamp_lte: midTimestamp }, }); const events = flattenConnection(result.account.events); @@ -209,7 +209,7 @@ describe("Account.events filtering (AccountEventsWhereInput)", () => { const maxTs = allEvents[allEvents.length - 1].timestamp; const result = await request(AccountEventsFiltered, { - address: DEVNET_ACCOUNTS.deployer.address, + address: accounts.deployer.address, where: { timestamp_gte: minTs, timestamp_lte: maxTs }, first: 1000, }); @@ -226,7 +226,7 @@ describe("Account.events filtering (AccountEventsWhereInput)", () => { const midTimestamp = seedEvent.timestamp; const result = await request(AccountEventsFiltered, { - address: DEVNET_ACCOUNTS.deployer.address, + address: accounts.deployer.address, where: { selector_in: [targetSelector], timestamp_gte: midTimestamp }, }); const events = flattenConnection(result.account.events); @@ -243,7 +243,7 @@ describe("Account.events filtering (AccountEventsWhereInput)", () => { const maxTimestamp = BigInt(allEvents[allEvents.length - 1].timestamp); const result = await request(AccountEventsFiltered, { - address: DEVNET_ACCOUNTS.deployer.address, + address: accounts.deployer.address, where: { timestamp_gte: (maxTimestamp + 1n).toString() }, }); const events = flattenConnection(result.account.events); diff --git a/packages/datasources/package.json b/packages/datasources/package.json index 9eb98c5545..83acc86b0e 100644 --- a/packages/datasources/package.json +++ b/packages/datasources/package.json @@ -18,14 +18,21 @@ "dist" ], "exports": { - ".": "./src/index.ts" + ".": "./src/index.ts", + "./devnet": "./src/devnet/index.ts" }, "sideEffects": false, "publishConfig": { "access": "public", "exports": { - "default": "./dist/index.js", - "types": "./dist/index.d.ts" + ".": { + "default": "./dist/index.js", + "types": "./dist/index.d.ts" + }, + "./devnet": { + "default": "./dist/devnet/index.js", + "types": "./dist/devnet/index.d.ts" + } }, "main": "./dist/index.js", "module": "./dist/index.mjs", diff --git a/packages/datasources/src/devnet/constants.ts b/packages/datasources/src/devnet/constants.ts new file mode 100644 index 0000000000..f0c6573c7d --- /dev/null +++ b/packages/datasources/src/devnet/constants.ts @@ -0,0 +1,131 @@ +import type { Address, Hex } from "viem"; +import { mnemonicToAccount } from "viem/accounts"; + +type NormalizedAddress = Lowercase
; + +function toNormalizedAddress(address: Address): NormalizedAddress { + return address.toLowerCase() as NormalizedAddress; +} + +/** + * Deterministic contract addresses for the ENS contracts-v2 devnet used by ens-test-env. + * Source of truth is the devnet deployment used by this repository's test harness and compose setup: + * @see https://github.com/ensdomains/contracts-v2 + * @see https://github.com/ensdomains/ens-test-env + */ +export const contracts = { + // -- DNS -- + dnssecGatewayProvider: "0x5fbdb2315678afecb367f032d93f642f64180aa3", + dnsTxtResolver: "0xe7f1725e7734ce288f8367e1bb143e90bb3f0512", + dnsAliasResolver: "0x322813fd9a801c5507c9de605d63cea4f2ce6c44", + dnsTldResolver: "0x998abeb3e57409262ae5b751f60747921b33613e", + offchainDnsResolver: "0x851356ae760d987e095750cceb3bc6014560891c", + simplePublicSuffixList: "0xf5059a5d33d5853360d16c683c16e67980206f36", + dnsRegistrar: "0x202cce504e04bed6fc0521238ddf04bc9e8e15ab", + extendedDnsResolver: "0x4631bcabd6df18d94796344963cb60d44a4136b6", + + // -- Registries -- + legacyEnsRegistry: "0x9fe46736679d2d9a65f0992f2272de9f3c7fa6e0", + ensRegistry: "0xcf7ed3acca5a467e9e704c703e8d87f634fb0fc9", + rootRegistry: "0x2279b7a0a67db372996a5fab50d91eaa73d2ebe6", + ethRegistry: "0x8f86403a4de0bb5791fa46b8e795c547942fe4cf", + reverseRegistry: "0xcd8a1c3ba11cf5ecfa6267617243239504a98d90", + + // -- Registrars & Controllers -- + baseRegistrar: "0x8a791620dd6260079bf849dc5567adc3f2fdc318", + ethRegistrar: "0x4c4a2f8c81640e47606d3fd77b353e87ba015584", + legacyEthRegistrarController: "0xfbc22278a96299d91d41c453234d97b4f5eb9b2d", + wrappedEthRegistrarController: "0x253553366da8546fc250f225fe3d25d0c782303b", + ethRegistrarController: "0x1c85638e118b37167e9298c2268758e058ddfda0", + batchRegistrar: "0xd8a5a9b31c3c0232e196d518e89fd8bf83acad43", + + // -- Reverse Resolution -- + ethReverseRegistrar: "0x59b670e9fa9d0a427751af201d676719a970857b", + defaultReverseRegistrar: "0x4c5859f0f772848b2d91f1d83e2fe57935348029", + defaultReverseResolver: "0x5f3f1dbd7b74c6b46e8c44f98792a1daf8d69154", + ethReverseResolver: "0x7bc06c482dead17c0e297afbc32f6e63d3846650", + reverseRegistrar: "0x162a433068f51e18b7d13932f27e66a3f99e6890", + l2ReverseRegistrar: "0x49fd2be640db2910c2fab69bb8531ab6e76127ff", + + // -- Resolvers -- + ensv1Resolver: "0x5fc8d32690cc91d4c39d9d3abcbd16989f875707", + ensv2Resolver: "0xc6e7df5e7b4f2a278906862b61205850344d4e7d", + ownedResolver: "0x68b1d87f95878fe05b998f19b66f4baba5de1aed", + permissionedResolver: "0x5ea90acf6555276660760fe629d72932c91f4b8e", + legacyPublicResolver: "0x86a2ee8faf9a840f7a2c64ca3d51209f9a02081d", + publicResolver: "0xa4899d35897033b927acfcf422bc745916139776", + permissionedResolverImpl: "0x809d550fca64d94bd9f66e60752a544199cfac3d", + universalResolver: "0x5067457698fd6fa1c6964e416b3f42713513b3dd", + universalResolverV2: "0x8198f5d8f8cffe8f9c413d98a0a55aeb8ab9fbb7", + upgradableUniversalResolverProxy: "0x0355b7b8cb128fa5692729ab3aaa199c1753f726", + + // -- L2 Reverse Resolvers -- + arbitrumReverseResolver: "0xf953b3a269d80e3eb0f2947630da976b896a8c5b", + baseReverseResolver: "0xaa292e8611adf267e563f334ee42320ac96d0463", + lineaReverseResolver: "0x5c74c94173f05da1720953407cbb920f3df9f887", + optimismReverseResolver: "0x720472c8ce72c2a2d711333e064abd3e6bbeadd3", + scrollReverseResolver: "0xe8d2a1e88c91dcd5433208d4152cc4f399a7e91d", + + // -- Infrastructure -- + batchGatewayProvider: "0xdc64a140aa3e981100a9beca4e685f962f0cf6c9", + hcaFactory: "0x0165878a594ca255338adfa4d48449f69242eb8f", + simpleRegistryMetadata: "0xa513e6e4b8f2a923d98304ec87f64353c4d5c853", + root: "0x610178da211fef7d417bc0e6fed39f05609ad788", + rootSecurityController: "0xb7f8bc63bbcad18155201308c8f3540b07f84f5e", + registrarSecurityController: "0x0b306bf915c4d645ff596e518faf3f9669b97016", + verifiableFactory: "0x4ed7c70f96b99c776995fb64377f0d4ab3b0e1c1", + nameWrapper: "0x5081a39b8a5f0e35a8d959395a630b68b74dd30f", + unlockedMigrationController: "0xdbc43ba45381e02825b14322cddd15ec4b3164e6", + wrapperRegistry: "0x2e2ed0cfd3ad2f1d34481277b3204d807ca2f8c2", + lockedMigrationController: "0x51a1ceb83b83f1985a81c295d1ff28afef186e02", + userRegistry: "0x7969c5ed335650692bc04293b07f5bf2e7a673c0", + staticMetadataService: "0xb0d4afd8879ed9f52b28595d31b441d079b2ca07", + multicall3: "0xca11bde05977b3631167028862be2a173976ca11", + migrationHelper: "0x18e317a7d70d8fbf8e6e893616b52390ebbdb629", + + // -- DNSSEC Algorithms & Digests -- + rsasha1Algorithm: "0xa85233c63b9ee964add6f2cffe00fd84eb32338f", + rsasha256Algorithm: "0x4a679253410272dd5232b3ff7cf5dbb88f295319", + p256sha256Algorithm: "0x7a2088a1bfc9d81c55368ae168c2c02570cb814f", + sha1Digest: "0x09635f643e140090a9a8dcd712ed6285858cebef", + sha256Digest: "0xc5a5c42992decbae36851359345fe25997f5c42d", + dnssecImpl: "0x67d269191c92caf3cd7723f116c85e6e9bf55933", + + // -- Pricing -- + standardRentPriceOracle: "0x1429859428c0abc9c2c47c8ee9fbaf82cfa0f20f", + staticBulkRenewal: "0x4c2f7092c2ae51d986befee378e50bd4db99c901", + dummyOracle: "0xd84379ceae14aa33c123af12424a37803f885889", + exponentialPremiumPriceOracle: "0x2b0d36facd61b71cc05ab8f3d2355ec3631c0dd5", + + // -- Mock Tokens -- + mockUsdc: "0xfd471836031dc5108809d173a067e8486b9047a3", + mockDai: "0xcbeaf3bde82155f56486fb5a1072cb8baaf547cc", +} as const satisfies Record; + +const mnemonic = "test test test test test test test test test test test junk"; + +function createAccount(addressIndex: number) { + const account = mnemonicToAccount(mnemonic, { addressIndex }); + return { ...account, address: toNormalizedAddress(account.address) }; +} + +export const accounts = { + deployer: createAccount(0), + owner: createAccount(1), + user: createAccount(2), + user2: createAccount(3), +} as const; + +export const addresses = { + one: toNormalizedAddress(`0x${"1".repeat(40)}` as Address), +} as const satisfies Record; + +export const fixtures = { + abiBytes: `0x${"01".repeat(32)}`, + fourBytesInterface: "0x11100111", + publicKeyX: `0x${"02".repeat(32)}`, + publicKeyY: `0x${"03".repeat(32)}`, + contenthash: `0x${"04".repeat(32)}`, + bitcoinAddress: `0x${"05".repeat(25)}`, + litecoinAddress: `0x${"06".repeat(25)}`, +} as const satisfies Record; diff --git a/packages/datasources/src/devnet/index.ts b/packages/datasources/src/devnet/index.ts new file mode 100644 index 0000000000..b04bfcf75e --- /dev/null +++ b/packages/datasources/src/devnet/index.ts @@ -0,0 +1 @@ +export * from "./constants"; diff --git a/packages/datasources/src/ens-test-env.ts b/packages/datasources/src/devnet/namespace.ts similarity index 64% rename from packages/datasources/src/ens-test-env.ts rename to packages/datasources/src/devnet/namespace.ts index b27f4b9047..416569235d 100644 --- a/packages/datasources/src/ens-test-env.ts +++ b/packages/datasources/src/devnet/namespace.ts @@ -1,24 +1,25 @@ import { zeroAddress } from "viem"; -import { EnhancedAccessControl } from "./abis/ensv2/EnhancedAccessControl"; -import { ETHRegistrar } from "./abis/ensv2/ETHRegistrar"; -import { Registry } from "./abis/ensv2/Registry"; -import { UniversalResolverV2 } from "./abis/ensv2/UniversalResolverV2"; +import { EnhancedAccessControl } from "../abis/ensv2/EnhancedAccessControl"; +import { ETHRegistrar } from "../abis/ensv2/ETHRegistrar"; +import { Registry } from "../abis/ensv2/Registry"; +import { UniversalResolverV2 } from "../abis/ensv2/UniversalResolverV2"; // ABIs for ENSRoot Datasource -import { BaseRegistrar as root_BaseRegistrar } from "./abis/root/BaseRegistrar"; -import { LegacyEthRegistrarController as root_LegacyEthRegistrarController } from "./abis/root/LegacyEthRegistrarController"; -import { NameWrapper as root_NameWrapper } from "./abis/root/NameWrapper"; -import { Registry as root_Registry } from "./abis/root/Registry"; -import { UniversalRegistrarRenewalWithReferrer as root_UniversalRegistrarRenewalWithReferrer } from "./abis/root/UniversalRegistrarRenewalWithReferrer"; -import { UniversalResolverV1 } from "./abis/root/UniversalResolverV1"; -import { UnwrappedEthRegistrarController as root_UnwrappedEthRegistrarController } from "./abis/root/UnwrappedEthRegistrarController"; -import { WrappedEthRegistrarController as root_WrappedEthRegistrarController } from "./abis/root/WrappedEthRegistrarController"; -import { StandaloneReverseRegistrar } from "./abis/shared/StandaloneReverseRegistrar"; -import { ensTestEnvChain } from "./lib/chains"; +import { BaseRegistrar as root_BaseRegistrar } from "../abis/root/BaseRegistrar"; +import { LegacyEthRegistrarController as root_LegacyEthRegistrarController } from "../abis/root/LegacyEthRegistrarController"; +import { NameWrapper as root_NameWrapper } from "../abis/root/NameWrapper"; +import { Registry as root_Registry } from "../abis/root/Registry"; +import { UniversalRegistrarRenewalWithReferrer as root_UniversalRegistrarRenewalWithReferrer } from "../abis/root/UniversalRegistrarRenewalWithReferrer"; +import { UniversalResolverV1 } from "../abis/root/UniversalResolverV1"; +import { UnwrappedEthRegistrarController as root_UnwrappedEthRegistrarController } from "../abis/root/UnwrappedEthRegistrarController"; +import { WrappedEthRegistrarController as root_WrappedEthRegistrarController } from "../abis/root/WrappedEthRegistrarController"; +import { StandaloneReverseRegistrar } from "../abis/shared/StandaloneReverseRegistrar"; +import { ensTestEnvChain } from "../lib/chains"; // Shared ABIs -import { ResolverABI } from "./lib/ResolverABI"; +import { ResolverABI } from "../lib/ResolverABI"; // Types -import { DatasourceNames, type ENSNamespace } from "./lib/types"; +import { DatasourceNames, type ENSNamespace } from "../lib/types"; +import { contracts } from "./constants"; /** * The ens-test-env ENSNamespace @@ -45,13 +46,13 @@ export default { // NOTE: named LegacyENSRegistry in devnet ENSv1RegistryOld: { abi: root_Registry, // Registry was redeployed, same abi - address: "0x9fe46736679d2d9a65f0992f2272de9f3c7fa6e0", + address: contracts.legacyEnsRegistry, startBlock: 0, }, // NOTE: named ENSRegistry in devnet ENSv1Registry: { abi: root_Registry, // Registry was redeployed, same abi - address: "0xcf7ed3acca5a467e9e704c703e8d87f634fb0fc9", + address: contracts.ensRegistry, startBlock: 0, }, Resolver: { @@ -61,25 +62,25 @@ export default { // NOTE: named BaseRegistrarImplementation in devnet BaseRegistrar: { abi: root_BaseRegistrar, - address: "0x8a791620dd6260079bf849dc5567adc3f2fdc318", + address: contracts.baseRegistrar, startBlock: 0, }, // NOTE: named LegacyETHRegistrarController in devnet LegacyEthRegistrarController: { abi: root_LegacyEthRegistrarController, - address: "0xfbc22278a96299d91d41c453234d97b4f5eb9b2d", + address: contracts.legacyEthRegistrarController, startBlock: 0, }, // NOTE: named WrappedETHRegistrarController in devnet WrappedEthRegistrarController: { abi: root_WrappedEthRegistrarController, - address: "0x253553366da8546fc250f225fe3d25d0c782303b", + address: contracts.wrappedEthRegistrarController, startBlock: 0, }, // NOTE: named ETHRegistrarController in devnet UnwrappedEthRegistrarController: { abi: root_UnwrappedEthRegistrarController, - address: "0x1c85638e118b37167e9298c2268758e058ddfda0", + address: contracts.ethRegistrarController, startBlock: 0, }, // NOTE: not in devnet, set to zeroAddress @@ -90,18 +91,18 @@ export default { }, NameWrapper: { abi: root_NameWrapper, - address: "0x5081a39b8a5f0e35a8d959395a630b68b74dd30f", + address: contracts.nameWrapper, startBlock: 0, }, UniversalResolver: { abi: UniversalResolverV1, - address: "0x5067457698fd6fa1c6964e416b3f42713513b3dd", + address: contracts.universalResolver, startBlock: 0, }, // NOTE: named UniversalResolverV2 in devnet UniversalResolverV2: { abi: UniversalResolverV2, - address: "0x8198f5d8f8cffe8f9c413d98a0a55aeb8ab9fbb7", + address: contracts.universalResolverV2, startBlock: 0, }, }, @@ -115,17 +116,17 @@ export default { EnhancedAccessControl: { abi: EnhancedAccessControl, startBlock: 0 }, RootRegistry: { abi: Registry, - address: "0x2279b7a0a67db372996a5fab50d91eaa73d2ebe6", + address: contracts.rootRegistry, startBlock: 0, }, ETHRegistry: { abi: Registry, - address: "0x8f86403a4de0bb5791fa46b8e795c547942fe4cf", + address: contracts.ethRegistry, startBlock: 0, }, ETHRegistrar: { abi: ETHRegistrar, - address: "0x4c4a2f8c81640e47606d3fd77b353e87ba015584", + address: contracts.ethRegistrar, startBlock: 0, }, }, @@ -136,28 +137,28 @@ export default { contracts: { DefaultReverseRegistrar: { abi: StandaloneReverseRegistrar, - address: "0x4c5859f0f772848b2d91f1d83e2fe57935348029", + address: contracts.defaultReverseRegistrar, startBlock: 0, }, // NOTE: named DefaultReverseResolver in devnet DefaultReverseResolver3: { abi: ResolverABI, - address: "0x5f3f1dbd7b74c6b46e8c44f98792a1daf8d69154", + address: contracts.defaultReverseResolver, startBlock: 0, }, // NOTE: named LegacyPublicResolver in devnet DefaultPublicResolver4: { abi: ResolverABI, - address: "0x86a2ee8faf9a840f7a2c64ca3d51209f9a02081d", + address: contracts.legacyPublicResolver, startBlock: 0, }, // NOTE: named PublicResolver in devnet DefaultPublicResolver5: { abi: ResolverABI, - address: "0xa4899d35897033b927acfcf422bc745916139776", + address: contracts.publicResolver, startBlock: 0, }, }, diff --git a/packages/datasources/src/namespaces.ts b/packages/datasources/src/namespaces.ts index 2eeba45f82..05f9491b10 100644 --- a/packages/datasources/src/namespaces.ts +++ b/packages/datasources/src/namespaces.ts @@ -1,4 +1,4 @@ -import ensTestEnv from "./ens-test-env"; +import ensTestEnv from "./devnet/namespace"; import { type DatasourceName, DatasourceNames, diff --git a/packages/datasources/tsup.config.ts b/packages/datasources/tsup.config.ts index 8fe92ca937..370bf11678 100644 --- a/packages/datasources/tsup.config.ts +++ b/packages/datasources/tsup.config.ts @@ -1,7 +1,7 @@ import { defineConfig } from "tsup"; export default defineConfig({ - entry: ["src/index.ts"], + entry: ["src/index.ts", "src/devnet/index.ts"], platform: "browser", format: ["esm"], target: "es2022", diff --git a/packages/ensnode-sdk/src/internal.ts b/packages/ensnode-sdk/src/internal.ts index 997a2bef8c..15510afee2 100644 --- a/packages/ensnode-sdk/src/internal.ts +++ b/packages/ensnode-sdk/src/internal.ts @@ -39,8 +39,6 @@ export * from "./shared/config/zod-schemas"; export * from "./shared/config-templates"; export * from "./shared/datasources-with-ensv2-contracts"; export * from "./shared/datasources-with-resolvers"; -export * from "./shared/devnet/addresses"; -export * from "./shared/devnet/data"; export * from "./shared/interpretation/interpret-address"; export * from "./shared/interpretation/interpret-record-values"; export * from "./shared/interpretation/interpret-resolver-values"; diff --git a/packages/ensnode-sdk/src/omnigraph-api/example-queries.ts b/packages/ensnode-sdk/src/omnigraph-api/example-queries.ts index a48842e523..73baa5d74a 100644 --- a/packages/ensnode-sdk/src/omnigraph-api/example-queries.ts +++ b/packages/ensnode-sdk/src/omnigraph-api/example-queries.ts @@ -1,9 +1,9 @@ import { asInterpretedName, toNormalizedAddress } from "enssdk"; import { DatasourceNames, ENSNamespaceIds } from "@ensnode/datasources"; +import { accounts } from "@ensnode/datasources/devnet"; import { maybeGetDatasourceContract } from "../shared/datasource-contract"; -import { DEVNET_ACCOUNTS } from "../shared/devnet/addresses"; import type { NamespaceSpecificValue } from "../shared/namespace-specific-value"; const SEPOLIA_V2_V2_ETH_REGISTRY = maybeGetDatasourceContract( @@ -181,7 +181,7 @@ query AccountDomains( }`, variables: { default: { address: VITALIK_ADDRESS }, - [ENSNamespaceIds.EnsTestEnv]: { address: DEVNET_ACCOUNTS.owner.address }, + [ENSNamespaceIds.EnsTestEnv]: { address: accounts.owner.address }, }, }, @@ -199,7 +199,7 @@ query AccountEvents( }`, variables: { default: { address: VITALIK_ADDRESS }, - [ENSNamespaceIds.EnsTestEnv]: { address: DEVNET_ACCOUNTS.deployer.address }, + [ENSNamespaceIds.EnsTestEnv]: { address: accounts.deployer.address }, }, }, @@ -282,7 +282,7 @@ query PermissionsByUser($address: Address!) { } }`, variables: { - default: { address: DEVNET_ACCOUNTS.deployer.address }, + default: { address: accounts.deployer.address }, // TODO: figure out a good sepolia-v2 user address // [ENSNamespaceIds.SepoliaV2]: { address: "" }, }, @@ -309,7 +309,7 @@ query AccountResolverPermissions($address: Address!) { } }`, variables: { - default: { address: DEVNET_ACCOUNTS.deployer.address }, + default: { address: accounts.deployer.address }, // TODO: figure out a good sepolia-v2 user address // [ENSNamespaceIds.SepoliaV2]: { address: "" }, }, diff --git a/packages/ensnode-sdk/src/shared/devnet/addresses.ts b/packages/ensnode-sdk/src/shared/devnet/addresses.ts deleted file mode 100644 index a06421eccf..0000000000 --- a/packages/ensnode-sdk/src/shared/devnet/addresses.ts +++ /dev/null @@ -1,129 +0,0 @@ -import { toNormalizedAddress } from "enssdk"; -import { mnemonicToAccount } from "viem/accounts"; - -/** - * Deterministic contract addresses from the ens-test-env devnet. - * These addresses are produced by the Hardhat/Anvil deploy scripts in contracts-v2 - * and are stable as long as the mnemonic and deploy order remain unchanged. - * - * Source: `pnpm devnet` output table - * @see https://github.com/ensdomains/contracts-v2 - */ - -export const DEVNET_CONTRACTS = { - // -- DNS -- - dnssecGatewayProvider: "0x5FbDB2315678afecb367f032d93F642f64180aa3", - dnsTxtResolver: "0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512", - dnsAliasResolver: "0x322813Fd9A801c5507c9de605d63CEA4f2CE6c44", - dnsTldResolver: "0x998abeb3E57409262aE5b751f60747921B33613E", - offchainDnsResolver: "0x851356ae760d987E095750cCeb3bC6014560891C", - simplePublicSuffixList: "0xf5059a5D33d5853360D16C683c16e67980206f36", - dnsRegistrar: "0x202CCe504e04bEd6fC0521238dDf04Bc9E8E15aB", - extendedDnsResolver: "0x4631BCAbD6dF18D94796344963cB60d44a4136b6", - - // -- Registries -- - legacyEnsRegistry: "0x9fE46736679d2D9a65F0992F2272dE9f3c7fa6e0", - ensRegistry: "0xCf7Ed3AccA5a467e9e704C703E8D87F634fB0Fc9", - rootRegistry: "0x2279B7A0a67DB372996a5FaB50D91eAA73d2eBe6", - ethRegistry: "0x8f86403A4DE0BB5791fa46B8e795C547942fE4Cf", - reverseRegistry: "0xCD8a1C3ba11CF5ECfa6267617243239504a98d90", - - // -- Registrars & Controllers -- - baseRegistrar: "0x8A791620dd6260079BF849Dc5567aDC3F2FdC318", - ethRegistrar: "0x4C4a2f8c81640e47606d3fd77B353E87Ba015584", - legacyEthRegistrarController: "0xfbC22278A96299D91d41C453234d97b4F5Eb9B2d", - wrappedEthRegistrarController: "0x253553366Da8546fC250F225fe3d25d0C782303b", - ethRegistrarController: "0x1c85638e118b37167e9298c2268758e058DdfDA0", - batchRegistrar: "0xD8a5a9b31c3C0232E196d518E89Fd8bF83AcAd43", - - // -- Reverse Resolution -- - ethReverseRegistrar: "0x59b670e9fA9D0A427751Af201D676719a970857b", - defaultReverseRegistrar: "0x4c5859f0F772848b2D91F1D83E2Fe57935348029", - defaultReverseResolver: "0x5f3f1dBD7B74C6B46e8c44f98792A1dAf8d69154", - ethReverseResolver: "0x7bc06c482DEAd17c0e297aFbC32f6e63d3846650", - reverseRegistrar: "0x162A433068F51e18b7d13932F27e66a3f99E6890", - l2ReverseRegistrar: "0x49fd2BE640DB2910c2fAb69bB8531Ab6E76127ff", - - // -- Resolvers -- - ensv1Resolver: "0x5FC8d32690cc91D4c39d9d3abcBD16989F875707", - ensv2Resolver: "0xc6e7DF5E7b4f2A278906862b61205850344D4e7d", - ownedResolver: "0x68B1D87F95878fE05B998F19b66F4baba5De1aed", - permissionedResolver: "0x5eA90aCF6555276660760fE629D72932c91f4b8E", - legacyPublicResolver: "0x86A2EE8FAf9A840F7a2c64CA3d51209F9A02081D", - publicResolver: "0xA4899D35897033b927acFCf422bc745916139776", - permissionedResolverImpl: "0x809d550fca64d94Bd9F66E60752A544199cfAC3D", - universalResolver: "0x5067457698Fd6Fa1C6964e416b3f42713513B3dD", - universalResolverV2: "0x8198f5d8F8CfFE8f9C413d98a0A55aEB8ab9FbB7", - upgradableUniversalResolverProxy: "0x0355B7B8cb128fA5692729Ab3AAa199C1753f726", - - // -- L2 Reverse Resolvers -- - arbitrumReverseResolver: "0xf953b3A269d80e3eB0F2947630Da976B896A8C5b", - baseReverseResolver: "0xAA292E8611aDF267e563f334Ee42320aC96D0463", - lineaReverseResolver: "0x5c74c94173F05dA1720953407cbb920F3DF9f887", - optimismReverseResolver: "0x720472c8ce72c2A2D711333e064ABD3E6BbEAdd3", - scrollReverseResolver: "0xe8D2A1E88c91DCd5433208d4152Cc4F399a7e91d", - - // -- Infrastructure -- - batchGatewayProvider: "0xDc64a140Aa3E981100a9becA4E685f962f0cF6C9", - hcaFactory: "0x0165878A594ca255338adfa4d48449f69242Eb8F", - simpleRegistryMetadata: "0xa513E6E4b8f2a923D98304ec87F64353C4D5C853", - root: "0x610178dA211FEF7D417bC0e6FeD39F05609AD788", - rootSecurityController: "0xB7f8BC63BbcaD18155201308C8f3540b07f84F5e", - registrarSecurityController: "0x0B306BF915C4d645ff596e518fAf3F9669b97016", - verifiableFactory: "0x4ed7c70F96B99c776995fB64377f0d4aB3B0e1C1", - nameWrapper: "0x5081a39b8A5f0E35a8D959395a630b68B74Dd30f", - unlockedMigrationController: "0xdbC43Ba45381e02825b14322cDdd15eC4B3164E6", - wrapperRegistry: "0x2E2Ed0Cfd3AD2f1d34481277b3204d807Ca2F8c2", - lockedMigrationController: "0x51A1ceB83B83F1985a81C295d1fF28Afef186E02", - userRegistry: "0x7969c5eD335650692Bc04293B07F5BF2e7A673C0", - staticMetadataService: "0xB0D4afd8879eD9F52b28595d31B441D079B2Ca07", - multicall3: "0xcA11bde05977b3631167028862bE2a173976CA11", - migrationHelper: "0x18E317A7D70d8fBf8e6E893616b52390EbBdb629", - - // -- DNSSEC Algorithms & Digests -- - rsasha1Algorithm: "0xa85233C63b9Ee964Add6F2cffe00Fd84eb32338f", - rsasha256Algorithm: "0x4A679253410272dd5232B3Ff7cF5dbB88f295319", - p256sha256Algorithm: "0x7a2088a1bFc9d81c55368AE168C2C02570cB814F", - sha1Digest: "0x09635F643e140090A9A8Dcd712eD6285858ceBef", - sha256Digest: "0xc5a5C42992dECbae36851359345FE25997F5C42d", - dnssecImpl: "0x67d269191c92Caf3cD7723F116c85e6E9bf55933", - - // -- Pricing -- - standardRentPriceOracle: "0x1429859428C0aBc9C2C47C8Ee9FBaf82cFA0F20f", - staticBulkRenewal: "0x4C2F7092C2aE51D986bEFEe378e50BD4dB99C901", - dummyOracle: "0xD84379CEae14AA33C123Af12424A37803F885889", - exponentialPremiumPriceOracle: "0x2B0d36FACD61B71CC05ab8F3D2355ec3631C0dd5", - - // -- Mock Tokens -- - mockUsdc: "0xFD471836031dc5108809D173A067e8486B9047A3", - mockDai: "0xcbEAF3BDe82155F56486Fb5a1072cb8baAf547cc", -} as const; - -/** - * Standard Hardhat/Anvil mnemonic used by the ens-test-env devnet. - */ -const DEVNET_MNEMONIC = "test test test test test test test test test test test junk"; - -function deriveNormalizedAccount(addressIndex: number) { - const account = mnemonicToAccount(DEVNET_MNEMONIC, { addressIndex }); - return { ...account, address: toNormalizedAddress(account.address) }; -} - -/** - * Named signer accounts from the ens-test-env devnet. - * Derived from the standard Hardhat mnemonic at account indices 0-3. - */ -export const DEVNET_ACCOUNTS = { - deployer: deriveNormalizedAccount(0), - owner: deriveNormalizedAccount(1), - user: deriveNormalizedAccount(2), - user2: deriveNormalizedAccount(3), -} as const; - -/** - * Non-signer helper addresses for tests. - */ -export const DEVNET_ADDRESSES = { - /** Non-signer placeholder address (0x11...11) used where only an address literal is needed. */ - one: toNormalizedAddress(`0x${"1".repeat(40)}`), -} as const; diff --git a/packages/ensnode-sdk/src/shared/devnet/data.ts b/packages/ensnode-sdk/src/shared/devnet/data.ts deleted file mode 100644 index f974dfcd47..0000000000 --- a/packages/ensnode-sdk/src/shared/devnet/data.ts +++ /dev/null @@ -1,14 +0,0 @@ -import type { Hex } from "viem"; - -/** - * Bytes constants used only for seeding devnet and testing. - */ -export const DEVNET_BYTES = { - abiBytes: `0x${"01".repeat(32)}`, - fourBytesInterface: "0x11100111", - publicKeyX: `0x${"02".repeat(32)}`, - publicKeyY: `0x${"03".repeat(32)}`, - contenthash: `0x${"04".repeat(32)}`, - bitcoinAddress: `0x${"05".repeat(25)}`, - litecoinAddress: `0x${"06".repeat(25)}`, -} as const satisfies Record; diff --git a/packages/integration-test-env/src/seed/index.ts b/packages/integration-test-env/src/seed/index.ts index 19f36ca917..3ef4c7be12 100644 --- a/packages/integration-test-env/src/seed/index.ts +++ b/packages/integration-test-env/src/seed/index.ts @@ -10,7 +10,7 @@ import { } from "viem"; import { ensTestEnvChain } from "@ensnode/datasources"; -import { DEVNET_ACCOUNTS } from "@ensnode/ensnode-sdk/internal"; +import { accounts } from "@ensnode/datasources/devnet"; import { seedPrimaryNameRecords } from "./primary-names"; import { seedResolverRecords } from "./resolver-records"; @@ -37,10 +37,10 @@ function createDevnetWalletClients(rpcUrl: string): DevnetWalletClients { const makeClient = (account: Account): DevnetWalletClient => createDevnetWalletClient(transport, account); return { - deployer: makeClient(DEVNET_ACCOUNTS.deployer), - owner: makeClient(DEVNET_ACCOUNTS.owner), - user: makeClient(DEVNET_ACCOUNTS.user), - user2: makeClient(DEVNET_ACCOUNTS.user2), + deployer: makeClient(accounts.deployer), + owner: makeClient(accounts.owner), + user: makeClient(accounts.user), + user2: makeClient(accounts.user2), }; } diff --git a/packages/integration-test-env/src/seed/primary-names.ts b/packages/integration-test-env/src/seed/primary-names.ts index 9989a0cc92..67f181ae57 100644 --- a/packages/integration-test-env/src/seed/primary-names.ts +++ b/packages/integration-test-env/src/seed/primary-names.ts @@ -1,4 +1,4 @@ -import { DEVNET_CONTRACTS } from "@ensnode/ensnode-sdk/internal"; +import { contracts } from "@ensnode/datasources/devnet"; import { ethReverseRegistrarAbi } from "./abi"; import type { DevnetWalletClient, DevnetWalletClients } from "./index"; @@ -9,7 +9,7 @@ export async function seedPrimaryNameRecords(clients: DevnetWalletClients): Prom async function setPrimaryNameRecord(walletClient: DevnetWalletClient, name: string): Promise { const hash = await walletClient.writeContract({ - address: DEVNET_CONTRACTS.ethReverseRegistrar, + address: contracts.ethReverseRegistrar, abi: ethReverseRegistrarAbi, functionName: "setName", args: [name], diff --git a/packages/integration-test-env/src/seed/resolver-records.ts b/packages/integration-test-env/src/seed/resolver-records.ts index c178f418ba..295af8e702 100644 --- a/packages/integration-test-env/src/seed/resolver-records.ts +++ b/packages/integration-test-env/src/seed/resolver-records.ts @@ -1,13 +1,13 @@ import { type Address, type Hex, namehash, toHex } from "viem"; import { packetToBytes } from "viem/ens"; -import { DEVNET_ADDRESSES, DEVNET_BYTES, DEVNET_CONTRACTS } from "@ensnode/ensnode-sdk/internal"; +import { addresses, contracts, fixtures } from "@ensnode/datasources/devnet"; import { publicResolverAbi, universalResolverV2Abi } from "./abi"; import type { DevnetWalletClient, DevnetWalletClients } from "./index"; export async function seedResolverRecords(clients: DevnetWalletClients): Promise { - await seedResolverRecordsForName(clients, "test.eth", DEVNET_CONTRACTS.permissionedResolver); + await seedResolverRecordsForName(clients, "test.eth", contracts.permissionedResolver); } async function seedResolverRecordsForName( @@ -33,26 +33,26 @@ async function seedResolverRecordsForName( // Multi-coin addresses // Coin 0 = Bitcoin - await setMulticoinAddress(clients.owner, resolver, node, 0n, DEVNET_BYTES.bitcoinAddress); + await setMulticoinAddress(clients.owner, resolver, node, 0n, fixtures.bitcoinAddress); // Coin 2 = Litecoin - await setMulticoinAddress(clients.owner, resolver, node, 2n, DEVNET_BYTES.litecoinAddress); + await setMulticoinAddress(clients.owner, resolver, node, 2n, fixtures.litecoinAddress); // Scalar resolver records - await setContenthash(clients.owner, resolver, node, DEVNET_BYTES.contenthash); - await setPubkey(clients.owner, resolver, node, DEVNET_BYTES.publicKeyX, DEVNET_BYTES.publicKeyY); - await setAbi(clients.owner, resolver, node, 1n, DEVNET_BYTES.abiBytes); + await setContenthash(clients.owner, resolver, node, fixtures.contenthash); + await setPubkey(clients.owner, resolver, node, fixtures.publicKeyX, fixtures.publicKeyY); + await setAbi(clients.owner, resolver, node, 1n, fixtures.abiBytes); await setInterfaceImplementer( clients.owner, resolver, node, - DEVNET_BYTES.fourBytesInterface, - DEVNET_ADDRESSES.one, + fixtures.fourBytesInterface, + addresses.one, ); } async function findResolver(client: DevnetWalletClient, name: string): Promise
{ const [resolver] = await client.readContract({ - address: DEVNET_CONTRACTS.universalResolverV2, + address: contracts.universalResolverV2, abi: universalResolverV2Abi, functionName: "findResolver", args: [toHex(packetToBytes(name))], From f10c3a76f10cebf450cdf40aac87ceb5c993dac1 Mon Sep 17 00:00:00 2001 From: sevenzing Date: Thu, 30 Apr 2026 13:21:36 +0400 Subject: [PATCH 11/18] reuse abis and types from datasources --- packages/datasources/package.json | 3 +- .../src/abis/root/L2ReverseRegistrar.ts | 643 ++++++++++++++++++ packages/datasources/src/devnet/constants.ts | 8 +- packages/datasources/src/index.ts | 1 + packages/integration-test-env/src/seed/abi.ts | 23 - .../src/seed/primary-names.ts | 4 +- .../src/seed/resolver-records.ts | 16 +- pnpm-lock.yaml | 3 + 8 files changed, 661 insertions(+), 40 deletions(-) create mode 100644 packages/datasources/src/abis/root/L2ReverseRegistrar.ts delete mode 100644 packages/integration-test-env/src/seed/abi.ts diff --git a/packages/datasources/package.json b/packages/datasources/package.json index 83acc86b0e..2bfe527d91 100644 --- a/packages/datasources/package.json +++ b/packages/datasources/package.json @@ -56,6 +56,7 @@ "vitest": "catalog:" }, "dependencies": { - "@ponder/utils": "^0.2.18" + "@ponder/utils": "^0.2.18", + "enssdk": "workspace:*" } } diff --git a/packages/datasources/src/abis/root/L2ReverseRegistrar.ts b/packages/datasources/src/abis/root/L2ReverseRegistrar.ts new file mode 100644 index 0000000000..1838b9764e --- /dev/null +++ b/packages/datasources/src/abis/root/L2ReverseRegistrar.ts @@ -0,0 +1,643 @@ +import type { Abi } from "viem"; + +export const L2ReverseRegistrar = [ + { + type: "constructor", + inputs: [ + { + name: "chainId", + type: "uint256", + internalType: "uint256", + }, + { + name: "label", + type: "string", + internalType: "string", + }, + ], + stateMutability: "nonpayable", + }, + { + type: "function", + name: "CHAIN_ID", + inputs: [], + outputs: [ + { + name: "", + type: "uint256", + internalType: "uint256", + }, + ], + stateMutability: "view", + }, + { + type: "function", + name: "PARENT_NODE", + inputs: [], + outputs: [ + { + name: "", + type: "bytes32", + internalType: "bytes32", + }, + ], + stateMutability: "view", + }, + { + type: "function", + name: "inceptionOf", + inputs: [ + { + name: "addr", + type: "address", + internalType: "address", + }, + ], + outputs: [ + { + name: "inception", + type: "uint256", + internalType: "uint256", + }, + ], + stateMutability: "view", + }, + { + type: "function", + name: "name", + inputs: [ + { + name: "node", + type: "bytes32", + internalType: "bytes32", + }, + ], + outputs: [ + { + name: "", + type: "string", + internalType: "string", + }, + ], + stateMutability: "view", + }, + { + type: "function", + name: "nameForAddr", + inputs: [ + { + name: "addr", + type: "address", + internalType: "address", + }, + ], + outputs: [ + { + name: "", + type: "string", + internalType: "string", + }, + ], + stateMutability: "view", + }, + { + type: "function", + name: "resolve", + inputs: [ + { + name: "name_", + type: "bytes", + internalType: "bytes", + }, + { + name: "data", + type: "bytes", + internalType: "bytes", + }, + ], + outputs: [ + { + name: "", + type: "bytes", + internalType: "bytes", + }, + ], + stateMutability: "view", + }, + { + type: "function", + name: "setName", + inputs: [ + { + name: "name", + type: "string", + internalType: "string", + }, + ], + outputs: [], + stateMutability: "nonpayable", + }, + { + type: "function", + name: "setNameForAddr", + inputs: [ + { + name: "addr", + type: "address", + internalType: "address", + }, + { + name: "name", + type: "string", + internalType: "string", + }, + ], + outputs: [], + stateMutability: "nonpayable", + }, + { + type: "function", + name: "setNameForAddrWithSignature", + inputs: [ + { + name: "claim", + type: "tuple", + internalType: "struct IL2ReverseRegistrar.NameClaim", + components: [ + { + name: "name", + type: "string", + internalType: "string", + }, + { + name: "addr", + type: "address", + internalType: "address", + }, + { + name: "chainIds", + type: "uint256[]", + internalType: "uint256[]", + }, + { + name: "signedAt", + type: "uint256", + internalType: "uint256", + }, + ], + }, + { + name: "signature", + type: "bytes", + internalType: "bytes", + }, + ], + outputs: [], + stateMutability: "nonpayable", + }, + { + type: "function", + name: "setNameForOwnableWithSignature", + inputs: [ + { + name: "claim", + type: "tuple", + internalType: "struct IL2ReverseRegistrar.NameClaim", + components: [ + { + name: "name", + type: "string", + internalType: "string", + }, + { + name: "addr", + type: "address", + internalType: "address", + }, + { + name: "chainIds", + type: "uint256[]", + internalType: "uint256[]", + }, + { + name: "signedAt", + type: "uint256", + internalType: "uint256", + }, + ], + }, + { + name: "owner", + type: "address", + internalType: "address", + }, + { + name: "signature", + type: "bytes", + internalType: "bytes", + }, + ], + outputs: [], + stateMutability: "nonpayable", + }, + { + type: "function", + name: "supportsInterface", + inputs: [ + { + name: "interfaceID", + type: "bytes4", + internalType: "bytes4", + }, + ], + outputs: [ + { + name: "", + type: "bool", + internalType: "bool", + }, + ], + stateMutability: "view", + }, + { + type: "function", + name: "syncName", + inputs: [ + { + name: "addr", + type: "address", + internalType: "address", + }, + ], + outputs: [], + stateMutability: "nonpayable", + }, + { + type: "event", + name: "ExpiryUpdated", + inputs: [ + { + name: "tokenId", + type: "uint256", + indexed: true, + internalType: "uint256", + }, + { + name: "newExpiry", + type: "uint64", + indexed: true, + internalType: "uint64", + }, + { + name: "sender", + type: "address", + indexed: true, + internalType: "address", + }, + ], + anonymous: false, + }, + { + type: "event", + name: "LabelRegistered", + inputs: [ + { + name: "tokenId", + type: "uint256", + indexed: true, + internalType: "uint256", + }, + { + name: "labelHash", + type: "bytes32", + indexed: true, + internalType: "bytes32", + }, + { + name: "label", + type: "string", + indexed: false, + internalType: "string", + }, + { + name: "owner", + type: "address", + indexed: false, + internalType: "address", + }, + { + name: "expiry", + type: "uint64", + indexed: false, + internalType: "uint64", + }, + { + name: "sender", + type: "address", + indexed: true, + internalType: "address", + }, + ], + anonymous: false, + }, + { + type: "event", + name: "LabelReserved", + inputs: [ + { + name: "tokenId", + type: "uint256", + indexed: true, + internalType: "uint256", + }, + { + name: "labelHash", + type: "bytes32", + indexed: true, + internalType: "bytes32", + }, + { + name: "label", + type: "string", + indexed: false, + internalType: "string", + }, + { + name: "expiry", + type: "uint64", + indexed: false, + internalType: "uint64", + }, + { + name: "sender", + type: "address", + indexed: true, + internalType: "address", + }, + ], + anonymous: false, + }, + { + type: "event", + name: "LabelUnregistered", + inputs: [ + { + name: "tokenId", + type: "uint256", + indexed: true, + internalType: "uint256", + }, + { + name: "sender", + type: "address", + indexed: true, + internalType: "address", + }, + ], + anonymous: false, + }, + { + type: "event", + name: "NameChanged", + inputs: [ + { + name: "node", + type: "bytes32", + indexed: true, + internalType: "bytes32", + }, + { + name: "name", + type: "string", + indexed: false, + internalType: "string", + }, + ], + anonymous: false, + }, + { + type: "event", + name: "NameForAddrChanged", + inputs: [ + { + name: "addr", + type: "address", + indexed: true, + internalType: "address", + }, + { + name: "name", + type: "string", + indexed: false, + internalType: "string", + }, + ], + anonymous: false, + }, + { + type: "event", + name: "ParentUpdated", + inputs: [ + { + name: "parent", + type: "address", + indexed: true, + internalType: "contract IRegistry", + }, + { + name: "label", + type: "string", + indexed: false, + internalType: "string", + }, + { + name: "sender", + type: "address", + indexed: true, + internalType: "address", + }, + ], + anonymous: false, + }, + { + type: "event", + name: "ResolverUpdated", + inputs: [ + { + name: "tokenId", + type: "uint256", + indexed: true, + internalType: "uint256", + }, + { + name: "resolver", + type: "address", + indexed: true, + internalType: "address", + }, + { + name: "sender", + type: "address", + indexed: true, + internalType: "address", + }, + ], + anonymous: false, + }, + { + type: "event", + name: "SubregistryUpdated", + inputs: [ + { + name: "tokenId", + type: "uint256", + indexed: true, + internalType: "uint256", + }, + { + name: "subregistry", + type: "address", + indexed: true, + internalType: "contract IRegistry", + }, + { + name: "sender", + type: "address", + indexed: true, + internalType: "address", + }, + ], + anonymous: false, + }, + { + type: "event", + name: "TokenRegenerated", + inputs: [ + { + name: "oldTokenId", + type: "uint256", + indexed: true, + internalType: "uint256", + }, + { + name: "newTokenId", + type: "uint256", + indexed: true, + internalType: "uint256", + }, + ], + anonymous: false, + }, + { + type: "error", + name: "ChainIdsNotAscending", + inputs: [], + }, + { + type: "error", + name: "CurrentChainNotFound", + inputs: [ + { + name: "chainId", + type: "uint256", + internalType: "uint256", + }, + ], + }, + { + type: "error", + name: "InvalidSignature", + inputs: [], + }, + { + type: "error", + name: "LabelIsEmpty", + inputs: [], + }, + { + type: "error", + name: "LabelIsTooLong", + inputs: [ + { + name: "label", + type: "string", + internalType: "string", + }, + ], + }, + { + type: "error", + name: "NotOwnerOfContract", + inputs: [], + }, + { + type: "error", + name: "SignatureNotValidYet", + inputs: [ + { + name: "signedAt", + type: "uint256", + internalType: "uint256", + }, + { + name: "currentTime", + type: "uint256", + internalType: "uint256", + }, + ], + }, + { + type: "error", + name: "StaleSignature", + inputs: [ + { + name: "signedAt", + type: "uint256", + internalType: "uint256", + }, + { + name: "inception", + type: "uint256", + internalType: "uint256", + }, + ], + }, + { + type: "error", + name: "TimestampOutOfRange", + inputs: [ + { + name: "timestamp", + type: "uint256", + internalType: "uint256", + }, + ], + }, + { + type: "error", + name: "Unauthorized", + inputs: [], + }, + { + type: "error", + name: "UnreachableName", + inputs: [ + { + name: "name", + type: "bytes", + internalType: "bytes", + }, + ], + }, + { + type: "error", + name: "UnsupportedResolverProfile", + inputs: [ + { + name: "selector", + type: "bytes4", + internalType: "bytes4", + }, + ], + }, +] as const satisfies Abi; diff --git a/packages/datasources/src/devnet/constants.ts b/packages/datasources/src/devnet/constants.ts index f0c6573c7d..e3acb523c8 100644 --- a/packages/datasources/src/devnet/constants.ts +++ b/packages/datasources/src/devnet/constants.ts @@ -1,12 +1,8 @@ +import type { NormalizedAddress } from "enssdk"; +import { toNormalizedAddress } from "enssdk"; import type { Address, Hex } from "viem"; import { mnemonicToAccount } from "viem/accounts"; -type NormalizedAddress = Lowercase
; - -function toNormalizedAddress(address: Address): NormalizedAddress { - return address.toLowerCase() as NormalizedAddress; -} - /** * Deterministic contract addresses for the ENS contracts-v2 devnet used by ens-test-env. * Source of truth is the devnet deployment used by this repository's test harness and compose setup: diff --git a/packages/datasources/src/index.ts b/packages/datasources/src/index.ts index dd6d5c1672..37319822b5 100644 --- a/packages/datasources/src/index.ts +++ b/packages/datasources/src/index.ts @@ -1,6 +1,7 @@ export { EnhancedAccessControl as EnhancedAccessControlABI } from "./abis/ensv2/EnhancedAccessControl"; export { ETHRegistrar as ETHRegistrarABI } from "./abis/ensv2/ETHRegistrar"; export { Registry as RegistryABI } from "./abis/ensv2/Registry"; +export { L2ReverseRegistrar as L2ReverseRegistrarABI } from "./abis/root/L2ReverseRegistrar"; export { StandaloneReverseRegistrar as StandaloneReverseRegistrarABI } from "./abis/shared/StandaloneReverseRegistrar"; export { UniversalResolverABI } from "./abis/shared/UniversalResolver"; export { ThreeDNSToken as ThreeDNSTokenABI } from "./abis/threedns/ThreeDNSToken"; diff --git a/packages/integration-test-env/src/seed/abi.ts b/packages/integration-test-env/src/seed/abi.ts deleted file mode 100644 index 16e8ef3afa..0000000000 --- a/packages/integration-test-env/src/seed/abi.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { parseAbi } from "viem"; - -export const ethReverseRegistrarAbi = parseAbi([ - // https://github.com/ensdomains/contracts-v2/blob/42f2016e7ba87eb3854afe51ad55186a16b32a74/contracts/src/reverse-registrar/L2ReverseRegistrar.sol#L117-L119 - "function setName(string name) returns (bytes32)", -]); - -// https://github.com/ensdomains/contracts-v2/blob/42f2016e7ba87eb3854afe51ad55186a16b32a74/contracts/test/utils/resolutions.ts#L28 -export const publicResolverAbi = parseAbi([ - "function setName(bytes32 node, string newName)", - "function setText(bytes32 node, string key, string value)", - "function setAddr(bytes32 node, address a)", - "function setAddr(bytes32 node, uint256 coinType, bytes a)", - "function setContenthash(bytes32 node, bytes hash)", - "function setPubkey(bytes32 node, bytes32 x, bytes32 y)", - "function setABI(bytes32 node, uint256 contentType, bytes data)", - "function setInterface(bytes32 node, bytes4 interfaceID, address implementer)", - "function clearRecords(bytes32 node)", -]); - -export const universalResolverV2Abi = parseAbi([ - "function findResolver(bytes name) view returns (address resolver, bytes32 node, uint256 offset)", -]); diff --git a/packages/integration-test-env/src/seed/primary-names.ts b/packages/integration-test-env/src/seed/primary-names.ts index 67f181ae57..85228177e3 100644 --- a/packages/integration-test-env/src/seed/primary-names.ts +++ b/packages/integration-test-env/src/seed/primary-names.ts @@ -1,6 +1,6 @@ +import { L2ReverseRegistrarABI } from "@ensnode/datasources"; import { contracts } from "@ensnode/datasources/devnet"; -import { ethReverseRegistrarAbi } from "./abi"; import type { DevnetWalletClient, DevnetWalletClients } from "./index"; export async function seedPrimaryNameRecords(clients: DevnetWalletClients): Promise { @@ -10,7 +10,7 @@ export async function seedPrimaryNameRecords(clients: DevnetWalletClients): Prom async function setPrimaryNameRecord(walletClient: DevnetWalletClient, name: string): Promise { const hash = await walletClient.writeContract({ address: contracts.ethReverseRegistrar, - abi: ethReverseRegistrarAbi, + abi: L2ReverseRegistrarABI, functionName: "setName", args: [name], }); diff --git a/packages/integration-test-env/src/seed/resolver-records.ts b/packages/integration-test-env/src/seed/resolver-records.ts index 295af8e702..0822041c52 100644 --- a/packages/integration-test-env/src/seed/resolver-records.ts +++ b/packages/integration-test-env/src/seed/resolver-records.ts @@ -1,9 +1,9 @@ import { type Address, type Hex, namehash, toHex } from "viem"; import { packetToBytes } from "viem/ens"; +import { ResolverABI, UniversalResolverABI } from "@ensnode/datasources"; import { addresses, contracts, fixtures } from "@ensnode/datasources/devnet"; -import { publicResolverAbi, universalResolverV2Abi } from "./abi"; import type { DevnetWalletClient, DevnetWalletClients } from "./index"; export async function seedResolverRecords(clients: DevnetWalletClients): Promise { @@ -53,7 +53,7 @@ async function seedResolverRecordsForName( async function findResolver(client: DevnetWalletClient, name: string): Promise
{ const [resolver] = await client.readContract({ address: contracts.universalResolverV2, - abi: universalResolverV2Abi, + abi: UniversalResolverABI, functionName: "findResolver", args: [toHex(packetToBytes(name))], }); @@ -69,7 +69,7 @@ async function setTextRecord( ): Promise { const hash = await walletClient.writeContract({ address: resolver, - abi: publicResolverAbi, + abi: ResolverABI, functionName: "setText", args: [node, key, value], }); @@ -86,7 +86,7 @@ async function setMulticoinAddress( ): Promise { const hash = await walletClient.writeContract({ address: resolver, - abi: publicResolverAbi, + abi: ResolverABI, functionName: "setAddr", args: [node, coinType, addressBytes], }); @@ -102,7 +102,7 @@ async function setContenthash( ): Promise { const hash = await walletClient.writeContract({ address: resolver, - abi: publicResolverAbi, + abi: ResolverABI, functionName: "setContenthash", args: [node, hashValue], }); @@ -119,7 +119,7 @@ async function setPubkey( ): Promise { const hash = await walletClient.writeContract({ address: resolver, - abi: publicResolverAbi, + abi: ResolverABI, functionName: "setPubkey", args: [node, x, y], }); @@ -136,7 +136,7 @@ async function setAbi( ): Promise { const hash = await walletClient.writeContract({ address: resolver, - abi: publicResolverAbi, + abi: ResolverABI, functionName: "setABI", args: [node, contentType, data], }); @@ -153,7 +153,7 @@ async function setInterfaceImplementer( ): Promise { const hash = await walletClient.writeContract({ address: resolver, - abi: publicResolverAbi, + abi: ResolverABI, functionName: "setInterface", args: [node, interfaceId, implementer], }); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c747f9a55e..7d2e344a86 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -844,6 +844,9 @@ importers: '@ponder/utils': specifier: ^0.2.18 version: 0.2.18(typescript@5.9.3)(viem@2.38.5(typescript@5.9.3)(zod@4.3.6)) + enssdk: + specifier: workspace:* + version: link:../enssdk devDependencies: '@ensnode/shared-configs': specifier: workspace:* From 924274984401be97269d9e9373463ce6e2eb7448 Mon Sep 17 00:00:00 2001 From: sevenzing Date: Mon, 4 May 2026 14:48:10 +0400 Subject: [PATCH 12/18] fix bug with long wait for transaction --- packages/integration-test-env/src/seed/index.ts | 17 +++++++++++++++++ .../src/seed/primary-names.ts | 3 ++- .../src/seed/resolver-records.ts | 13 +++++++------ 3 files changed, 26 insertions(+), 7 deletions(-) diff --git a/packages/integration-test-env/src/seed/index.ts b/packages/integration-test-env/src/seed/index.ts index 3ef4c7be12..b8d916bca1 100644 --- a/packages/integration-test-env/src/seed/index.ts +++ b/packages/integration-test-env/src/seed/index.ts @@ -1,3 +1,4 @@ +import type { Hex } from "viem"; import { type Account, type Chain, @@ -44,6 +45,22 @@ function createDevnetWalletClients(rpcUrl: string): DevnetWalletClients { }; } +export const seedReceiptWaitOptions = { + pollingInterval: 50, + confirmations: 1, + timeout: 15_000, +} as const; + +export async function waitForTransactionReceipt( + client: DevnetWalletClient, + hash: Hex, +): Promise { + await client.waitForTransactionReceipt({ + hash, + ...seedReceiptWaitOptions, + }); +} + export async function seedDevnet(rpcUrl: string): Promise { const clients = createDevnetWalletClients(rpcUrl); await seedPrimaryNameRecords(clients); diff --git a/packages/integration-test-env/src/seed/primary-names.ts b/packages/integration-test-env/src/seed/primary-names.ts index 85228177e3..2b237d3dbc 100644 --- a/packages/integration-test-env/src/seed/primary-names.ts +++ b/packages/integration-test-env/src/seed/primary-names.ts @@ -2,6 +2,7 @@ import { L2ReverseRegistrarABI } from "@ensnode/datasources"; import { contracts } from "@ensnode/datasources/devnet"; import type { DevnetWalletClient, DevnetWalletClients } from "./index"; +import { waitForTransactionReceipt } from "./index"; export async function seedPrimaryNameRecords(clients: DevnetWalletClients): Promise { await setPrimaryNameRecord(clients.owner, "test.eth"); @@ -14,6 +15,6 @@ async function setPrimaryNameRecord(walletClient: DevnetWalletClient, name: stri functionName: "setName", args: [name], }); - await walletClient.waitForTransactionReceipt({ hash }); + await waitForTransactionReceipt(walletClient, hash); console.log(`[seed] setPrimaryNameRecord("${name}") tx: ${hash}`); } diff --git a/packages/integration-test-env/src/seed/resolver-records.ts b/packages/integration-test-env/src/seed/resolver-records.ts index 0822041c52..499cd12034 100644 --- a/packages/integration-test-env/src/seed/resolver-records.ts +++ b/packages/integration-test-env/src/seed/resolver-records.ts @@ -5,6 +5,7 @@ import { ResolverABI, UniversalResolverABI } from "@ensnode/datasources"; import { addresses, contracts, fixtures } from "@ensnode/datasources/devnet"; import type { DevnetWalletClient, DevnetWalletClients } from "./index"; +import { waitForTransactionReceipt } from "./index"; export async function seedResolverRecords(clients: DevnetWalletClients): Promise { await seedResolverRecordsForName(clients, "test.eth", contracts.permissionedResolver); @@ -73,7 +74,7 @@ async function setTextRecord( functionName: "setText", args: [node, key, value], }); - await walletClient.waitForTransactionReceipt({ hash }); + await waitForTransactionReceipt(walletClient, hash); console.log(`[seed] setText("${key}", "${value}") tx: ${hash}`); } @@ -90,7 +91,7 @@ async function setMulticoinAddress( functionName: "setAddr", args: [node, coinType, addressBytes], }); - await walletClient.waitForTransactionReceipt({ hash }); + await waitForTransactionReceipt(walletClient, hash); console.log(`[seed] setAddr(coinType=${coinType}) tx: ${hash}`); } @@ -106,7 +107,7 @@ async function setContenthash( functionName: "setContenthash", args: [node, hashValue], }); - await walletClient.waitForTransactionReceipt({ hash }); + await waitForTransactionReceipt(walletClient, hash); console.log(`[seed] setContenthash() tx: ${hash}`); } @@ -123,7 +124,7 @@ async function setPubkey( functionName: "setPubkey", args: [node, x, y], }); - await walletClient.waitForTransactionReceipt({ hash }); + await waitForTransactionReceipt(walletClient, hash); console.log(`[seed] setPubkey() tx: ${hash}`); } @@ -140,7 +141,7 @@ async function setAbi( functionName: "setABI", args: [node, contentType, data], }); - await walletClient.waitForTransactionReceipt({ hash }); + await waitForTransactionReceipt(walletClient, hash); console.log(`[seed] setABI(contentType=${contentType}) tx: ${hash}`); } @@ -157,6 +158,6 @@ async function setInterfaceImplementer( functionName: "setInterface", args: [node, interfaceId, implementer], }); - await walletClient.waitForTransactionReceipt({ hash }); + await waitForTransactionReceipt(walletClient, hash); console.log(`[seed] setInterface(interfaceId=${interfaceId}) tx: ${hash}`); } From 48fdc11c9b97eca7772dbce9da404865b3eead32 Mon Sep 17 00:00:00 2001 From: sevenzing Date: Thu, 30 Apr 2026 18:27:35 +0400 Subject: [PATCH 13/18] commit idea file --- .memory-bank/tasks/0006-ens-test-kit/IDEA.md | 383 +++++++++++++++++++ 1 file changed, 383 insertions(+) create mode 100644 .memory-bank/tasks/0006-ens-test-kit/IDEA.md diff --git a/.memory-bank/tasks/0006-ens-test-kit/IDEA.md b/.memory-bank/tasks/0006-ens-test-kit/IDEA.md new file mode 100644 index 0000000000..1dc441d0da --- /dev/null +++ b/.memory-bank/tasks/0006-ens-test-kit/IDEA.md @@ -0,0 +1,383 @@ +# Task 0006: `@ensnode/ens-test-kit` — declarative ENS testing framework (Idea) + +## Status + +Proposal for review. Authored after iterative design conversation. Once approved, see [IMPL.md](./IMPL.md) for the PR-by-PR rollout. + +--- + +## Why we need this + +Previous idea described in [#1994](https://github.com/namehash/ensnode/pull/1994) with integration tests are imperative and triple-coupled (transport, fixture, assertion). + +Look at the heaviest case in [resolve-records.integration.test.ts](apps/ensapi/src/handlers/api/resolution/resolve-records.integration.test.ts): + +```ts +{ + description: "resolves every supported record type for test.eth", + name: "test.eth", + query: ["name=true", "addresses=60,0,2", "texts=avatar,...", ...].join("&"), + expectedStatus: 200, + expectedBody: { + records: { addresses: { 60: accounts.owner.address, 0: fixtures.bitcoinAddress, ... } }, + accelerationRequested: false, accelerationAttempted: false, + }, +} +``` + +Problems: + +1. **What is seeded on devnet** (in [packages/integration-test-env/src/seed/resolver-records.ts](packages/integration-test-env/src/seed/resolver-records.ts)) is implicit. You need to always go to check what data we have onchain to write testcase. + +2. **The query and expected body are HTTP-shaped.** Once we add resolution to Omnigraph, the same protocol-level case ("test.eth's avatar resolves to X") gets reimplemented as a GraphQL query with a different expected shape. + +3. **Adding a new edge case requires editing two distant places** (the seeder + the test file). There is no canonical "what does ENSNode's test environment contain?" catalog. + +4. We probably also want to test the `enssdk` wrapper, which would be yet another boilerplate layer. + +5. This approach cannot be reused by other ENS teams. A single shared catalog of test cases would invite the wider ENS community to contribute coverage upstream (PRs adding cases, forks adapting cases for their own resolvers) and grow our test suite collectively. + +So, summing it up, current idea does not scale. The omnigraph surface alone (Domains, Accounts, Resolvers, Registrations, plus future Resolution) will produce hundreds of cases. We need a structure that absorbs them without quadratic per-transport duplication. + +## What `@ensnode/ens-test-kit` is + +A new package with a single responsibility: **declarative ENS testing primitives**. + +It contains: +- **Narrow API interfaces** (`ResolutionsApi`, `DomainsApi`, `AccountsApi`, `ResolversApi`) — typed query surface that test cases call against. +- **Fixtures** — declarative chain-state preconditions. Each fixture's declaration type, builder, and on-chain handler live together in one file. +- **Seeder runtime** — applies fixtures to an `ens-test-env` devnet over RPC. +- **Test case catalog** — typed `TestCase[]` collections, organized by concern. +- **Vitest helper** — `runSuite(adapter, cases)` wires cases into a vitest suite. +- **`seed` CLI** — single binary that takes an RPC URL and applies the canonical fixture set. +- **Seeded-devnet Docker image** — extends `contracts-v2` and runs `seed` on container start, so a seeded devnet is one `docker compose up devnet` away. + +## What it is NOT + +- Not a production SDK. Interfaces exist *only* for testing; they are not how applications should talk to ENSNode. +- Not wired to ENSNode. The kit knows nothing about `postgres`, `ponder`, `ENSIndexer`, `ENSApi`. It runs against `devnet` with the contracts-v2 deployment and exports tooling to write ens test easily +- Not consrtaint to a transport. Each interface admits multiple implementations. + +## Where things live + +> NOTE: naming is in draft. I just want to show basic idea + +``` +packages/ + ens-test-kit/ NEW — universal, transport-agnostic + src/ + interfaces/ ResolutionsApi, DomainsApi, AccountsApi, ResolversApi + types/ shared shapes: Domain, Account, Resolver, Registration, ... + cases/ typed TestCase[] collections + resolution/{forward,reverse}.ts + domains/{by-name,subdomains,listing}.ts + accounts/owned-domains.ts + resolvers/indexed-records.ts + seeder/ + index.ts applies fixtures to a devnet + fixtures/ declaration + builder + on-chain handler per fixture kind, one file each + common.ts common canonical fixtures — the union of fixtures cases reference + primary-name.ts + text-record.ts + multicoin-address.ts + contenthash.ts + registration.ts + ... + vitest/ runSuite() helper + cli/ `seed` command implementation + bin/ + ens-test-kit CLI binary (single subcommand: `seed`) + devnet/ + Dockerfile extends contracts-v2; seeds on startup + entrypoint.sh runs runDevnet.ts + waits for health + runs seed + + integration-test-env/ EXISTING — slimmed + src/ + orchestrator.ts no longer seeds; devnet container does it + adapters/ NEW — implementations of kit's interfaces + omnigraph-adapter.ts implements DomainsApi, AccountsApi, ResolversApi + rest-adapter.ts implements ResolutionsApi + tests/ NEW — runSuite calls per concern × adapter + resolution-rest.integration.test.ts + domains-omnigraph.integration.test.ts + accounts-omnigraph.integration.test.ts + resolvers-omnigraph.integration.test.ts + +docker/ + services/ + devnet.yml UPDATED — points at the kit's seeded dockerfile +``` + +Dependency direction: + +``` +@ensnode/ens-test-kit (no ENSNode dependencies) + ▲ + │ + └── @ensnode/integration-test-env (orchestrator + adapters + test wiring) +``` + +## How a test case looks + +### The narrow interfaces + +```ts +interface ResolutionsApi { + resolveRecords(name: NormalizedName, selection: RecordsSelection): Promise; + resolvePrimaryName(address: Hex, chainId: ChainId): Promise; + resolvePrimaryNames(address: Hex, chainIds?: ChainId[]): Promise>; +} + +interface DomainsApi { + getDomainByName(name: NormalizedName): Promise; + getDomainByNamehash(node: Hex): Promise; + listDomains(where: DomainsWhere): Promise>; + getRegistration(name: NormalizedName): Promise; +} + +interface AccountsApi { + getAccount(address: Hex): Promise; +} + +interface ResolversApi { + getResolver(id: ResolverId): Promise; + listResolverRecords(name: NormalizedName): Promise<{ keys: string[]; coinTypes: ChainId[] }>; +} +``` + +Selection params are deliberately omitted in `ens-test-kit V1`. Adapters always over-fetch a stable shape (e.g. `Domain` includes `owner`, `registration`, `subdomains`). Tests use partial matching (`toMatchObject`) and only assert on what they care about. Field selection can be added later if the over-fetch becomes painful. + +### A `TestCase` is just data + +```ts +type TestCase = { + id: string; + description: string; + fixtures: Fixture[]; // declarative preconditions + call: (api: Api) => Promise; // what to perform over API. should be simple logic + expected: unknown; // partial shape to match against +}; +``` + +The case is generic in the API it requires. TypeScript enforces case-vs-adapter compatibility at compile time. No runtime tagging. + +### Example: a resolution case + +```ts +// ens-test-kit/src/cases/resolution/forward.ts +import { textRecord } from "../../seeder/fixtures/text-record"; +import type { ResolutionsApi, TestCase } from "../.."; + +const testEthAvatar = textRecord({ + id: "test-eth-avatar", + name: "test.eth", + key: "avatar", + value: "https://example.com/avatar.png", +}); + +export const forwardResolutionCases: TestCase[] = [ + { + id: "forward.text.test-eth-avatar", + description: "resolves avatar text record for test.eth", + fixtures: [testEthAvatar], + call: (api) => api.resolveRecords("test.eth", { texts: ["avatar"] }), + expected: { texts: { avatar: testEthAvatar.value } }, + }, + { + id: "forward.text.unset", + description: "returns null for unset text record", + fixtures: [], + call: (api) => api.resolveRecords("test.eth", { texts: ["nonexistent.key"] }), + expected: { texts: { "nonexistent.key": null } }, + }, +]; +``` + +`expected` derives from the fixture (`testEthAvatar.value`), so a change to seeded data automatically updates the expectation. Drift is impossible. + +### Example: a domains case (omnigraph-only) + +```ts +// ens-test-kit/src/cases/domains/subdomains.ts +import { registration } from "../../seeder/fixtures/registration"; +import type { DomainsApi, TestCase } from "../.."; + +const parentEth = registration({ id: "parent-eth", name: "parent.eth", owner: "owner" }); +const subParentEth = registration({ + id: "sub-parent-eth", + name: "sub.parent.eth", + owner: "owner", + parent: parentEth, +}); + +export const subdomainCases: TestCase[] = [ + { + id: "domains.subdomains.parent-eth-has-sub", + description: "parent.eth lists sub.parent.eth as a subdomain", + fixtures: [parentEth, subParentEth], + call: (api) => api.getDomainByName("parent.eth"), + expected: { + name: "parent.eth", + subdomains: [{ name: "sub.parent.eth" }], + }, + }, +]; +``` + +A case using both interfaces declares the intersection explicitly: + +```ts +// ens-test-kit/src/cases/accounts/owned-domains.ts +export const ownershipCases: TestCase[] = [ + { + id: "accounts.owns.owner-owns-test-eth", + description: "owner account's domains include test.eth", + fixtures: [/* references existing seeded fixtures */], + call: async (api) => { + const account = await api.getAccount(OWNER_ADDRESS); + return account?.domains.map((d) => d.name); + }, + expected: expect.arrayContaining(["test.eth"]), + }, +]; +``` + +## How a fixture looks + +A fixture's **declaration type**, **builder**, and **on-chain handler** all live in one file under `src/seeder/fixtures/`. Adding a new fixture kind = one new file (three exports) + one entry in the seeder dispatcher. + +```ts +// ens-test-kit/src/seeder/fixtures/text-record.ts +import { ResolverABI } from "@ensnode/datasources"; +import { contracts } from "@ensnode/datasources/devnet"; +import { namehash } from "viem"; +import type { NormalizedName } from "../../types"; +import type { SeederContext } from "../types"; + +export type TextRecordFixture = { + kind: "text-record"; + id: string; + name: NormalizedName; + resolverAddress?: Hex; + key: string; + value: string; +}; + +export function textRecord(args: Omit): TextRecordFixture { + return { kind: "text-record", ...args }; +} + +export async function applyTextRecord( + fixture: TextRecordFixture, + ctx: SeederContext, +): Promise { + const node = namehash(fixture.name); + const hash = await ctx.clients.owner.writeContract({ + address: fixture.resolverAddress ?? contracts.permissionedResolver, + abi: ResolverABI, + functionName: "setText", + args: [node, fixture.key, fixture.value], + }); + await ctx.clients.owner.waitForTransactionReceipt({ hash }); +} +``` + +This is mechanically the same logic that lives today in [packages/integration-test-env/src/seed/resolver-records.ts](packages/integration-test-env/src/seed/resolver-records.ts). The migration is purely a relocation + interface change (handler takes a `Fixture`, not raw args). + +### How seeding works end-to-end + +```ts +// ens-test-kit/src/seeder/index.ts +import { applyTextRecord } from "./fixtures/text-record"; +import { applyPrimaryName } from "./fixtures/primary-name"; +// ... + +const HANDLERS = { + "text-record": applyTextRecord, + "primary-name": applyPrimaryName, + "multicoin-address": applyMulticoinAddress, + "contenthash": applyContenthash, + "registration": applyRegistration, + // ... +} as const; + +export async function seedFixtures(rpcUrl: string, fixtures: Fixture[]): Promise { + const ctx = createSeederContext(rpcUrl); + const deduped = dedupeFixtures(fixtures); // by id + const ordered = topologicallySort(deduped); // registrations before records, etc. + for (const fixture of ordered) { + const handler = HANDLERS[fixture.kind]; + await handler(fixture as never, ctx); + } +} +``` + +The seeder is idempotent at the fixture level (same `id` → applied once). Topological ordering handles dependencies (you must register `parent.eth` before setting records on `sub.parent.eth`). + +## How seeding plugs into Docker + +We do not seed from `orchestrator.ts` anymore. The devnet container seeds itself on startup. + +1. `packages/ens-test-kit/devnet/Dockerfile` is multi-stage. The build stage compiles the kit (TypeScript → JS). The runtime stage extends `ghcr.io/ensdomains/contracts-v2:main-9f26a8f`, layers in Node and the built kit, and ships an `entrypoint.sh` that: + - Starts `./script/runDevnet.ts --testNames` in the background. + - Waits for Anvil to accept JSON-RPC on `localhost:8545`. + - Runs `ens-test-kit seed --rpc http://localhost:8545` against the local Anvil. + - Only then exposes the `localhost:8000/health` endpoint as healthy (so dependents wait for *seeded*, not just *anvil-booted*). + - `wait`s on the devnet process so the container stays up. +2. [docker/services/devnet.yml](docker/services/devnet.yml) is updated to build from this Dockerfile (build context = monorepo root, so the workspace lockfile is available). Once we publish a tagged image, this service can switch back to `image:`. +3. Anyone — CI, local devs, external users — gets a seeded devnet with `docker compose -f docker/services/devnet.yml up devnet`. No orchestrator, no ENSNode stack, no kit binary install. +4. [packages/integration-test-env/src/orchestrator.ts](packages/integration-test-env/src/orchestrator.ts) drops its `seedDevnet()` call entirely. The container handles seeding before its healthcheck reports healthy. + +The kit's only standalone CLI entry point is `ens-test-kit seed --rpc `. There is no `up` command — bringing up the chain is `docker compose ... up devnet`'s job. + +## What stays where + +1. **Fixture declarations + builders + handlers** + Today: [seed/primary-names.ts](packages/integration-test-env/src/seed/primary-names.ts), [seed/resolver-records.ts](packages/integration-test-env/src/seed/resolver-records.ts) + After: `ens-test-kit/seeder/fixtures/*` (one file per fixture kind, type+builder+handler combined). + +2. **`seedDevnet()` entry** + Today: [seed/index.ts](packages/integration-test-env/src/seed/index.ts), called from orchestrator + After: `ens-test-kit/seeder/index.ts`, called by the devnet container's entrypoint. + +3. **Devnet startup seeding** + Today: orchestrator phase 2 + After: devnet container entrypoint (before health passes). + +4. **Devnet constants (contract addrs, accounts, fixture values)** + Today: `@ensnode/datasources/devnet` + After: `@ensnode/ens-test-kit/devnet` is the source of truth; both kit internals and adapters import from it. + +5. **Positive-path integration test cases** + Today: `apps/ensapi/src/handlers/api/.../*.integration.test.ts` + After: `ens-test-kit/cases/*`; runners in `integration-test-env/tests/`. + +6. **Validation/4xx tests** + Today: same files + After: stay in `apps/ensapi` (transport-specific, not part of kit). + +7. **Orchestrator** + Today: seeds + runs services + After: runs services only; depends on devnet container being healthy = seeded. + +8. **`services/devnet.yml`** + Today: uses upstream contracts-v2 image + After: builds from kit's Dockerfile. + +## Design choices and rationale + +- **One global devnet, namespace-by-name.** Per-test isolation is impossible because Ponder reindexes on data change and snapshot/revert doesn't reset the indexer DB. All fixtures coexist on one devnet; conflicting scenarios use different names (`with-avatar.example.eth` vs `without-avatar.example.eth`). This matches what reference test envs (ensjs deploy scripts) effectively do. +- **Interfaces are test-only.** Production SDK stays focused; we don't accidentally promise these shapes to external consumers. If they prove valuable as a public API later, promotion is a separate decision. +- **Adapters live in `integration-test-env`.** They're glue between the kit and a running ENSNode stack — the only place they're meaningful. No `ensnode-sdk` coupling. +- **Over-fetch in V1, no `select` param.** Cases assert with `toMatchObject`. Adds selection later if/when over-fetched payloads get unwieldy. +- **TypeScript-first cases, no JSON/YAML format.** All target consumers are TS/JS. Compile-time validation of fixture references is worth more than format-language portability. +- **Seed inside the devnet container, not from the orchestrator.** A seeded devnet is the deliverable; once the kit's image is up, *any* consumer (CI, dev, external resolver team) gets the same seeded chain with one compose command. Removes orchestrator complexity and removes the dev-loop need to "boot the full ENSNode stack to get test data". +- **Single CLI command (`seed`).** No `up`, since `docker compose up devnet` already does that perfectly. +- **One file per fixture kind.** Declaration + builder + on-chain handler co-located: trivial to add a new fixture kind, easy to review, no file-jumping between declaration and runtime. + +## Out of scope + +- Real-chain (mainnet) validation — that's a follow-up; the kit's resolution adapter could later target viem/UniversalResolver to enable `resolution-eq`-style diffing in CI. +- ENSDb direct-access adapter (asserting indexed state by reading Postgres directly) — possible follow-up; not needed for V1. +- GraphQL subgraph-compat schema testing — possible future use of the kit; out of scope here. +- Schema-agnostic "fact assertions" DSL — over-engineering for the current case load. From ca23a1465fdc9bc37908b693f2b391499af49892 Mon Sep 17 00:00:00 2001 From: sevenzing Date: Fri, 1 May 2026 16:36:03 +0400 Subject: [PATCH 14/18] ens-test-kit initial --- .changeset/config.json | 3 +- packages/ens-test-kit/README.md | 13 +++ packages/ens-test-kit/package.json | 85 +++++++++++++++++++ packages/ens-test-kit/src/cases/index.ts | 1 + packages/ens-test-kit/src/cases/types.ts | 9 ++ packages/ens-test-kit/src/cli/index.ts | 4 + packages/ens-test-kit/src/index.ts | 6 ++ .../ens-test-kit/src/interfaces/accounts.ts | 5 ++ .../ens-test-kit/src/interfaces/domains.ts | 8 ++ packages/ens-test-kit/src/interfaces/index.ts | 4 + .../src/interfaces/resolutions.ts | 7 ++ .../ens-test-kit/src/interfaces/resolvers.ts | 6 ++ .../ens-test-kit/src/seeder/fixtures/abi.ts | 10 +++ .../src/seeder/fixtures/contenthash.ts | 9 ++ .../seeder/fixtures/interface-implementer.ts | 10 +++ .../src/seeder/fixtures/multicoin-address.ts | 10 +++ .../src/seeder/fixtures/primary-name.ts | 9 ++ .../src/seeder/fixtures/pubkey.ts | 10 +++ .../src/seeder/fixtures/text-record.ts | 10 +++ packages/ens-test-kit/src/seeder/index.ts | 8 ++ packages/ens-test-kit/src/seeder/types.ts | 29 +++++++ packages/ens-test-kit/src/types/index.ts | 69 +++++++++++++++ packages/ens-test-kit/src/vitest/index.ts | 3 + packages/ens-test-kit/tsconfig.json | 5 ++ packages/ens-test-kit/tsup.config.ts | 23 +++++ packages/ens-test-kit/vitest.config.ts | 8 ++ pnpm-lock.yaml | 21 +++++ 27 files changed, 384 insertions(+), 1 deletion(-) create mode 100644 packages/ens-test-kit/README.md create mode 100644 packages/ens-test-kit/package.json create mode 100644 packages/ens-test-kit/src/cases/index.ts create mode 100644 packages/ens-test-kit/src/cases/types.ts create mode 100644 packages/ens-test-kit/src/cli/index.ts create mode 100644 packages/ens-test-kit/src/index.ts create mode 100644 packages/ens-test-kit/src/interfaces/accounts.ts create mode 100644 packages/ens-test-kit/src/interfaces/domains.ts create mode 100644 packages/ens-test-kit/src/interfaces/index.ts create mode 100644 packages/ens-test-kit/src/interfaces/resolutions.ts create mode 100644 packages/ens-test-kit/src/interfaces/resolvers.ts create mode 100644 packages/ens-test-kit/src/seeder/fixtures/abi.ts create mode 100644 packages/ens-test-kit/src/seeder/fixtures/contenthash.ts create mode 100644 packages/ens-test-kit/src/seeder/fixtures/interface-implementer.ts create mode 100644 packages/ens-test-kit/src/seeder/fixtures/multicoin-address.ts create mode 100644 packages/ens-test-kit/src/seeder/fixtures/primary-name.ts create mode 100644 packages/ens-test-kit/src/seeder/fixtures/pubkey.ts create mode 100644 packages/ens-test-kit/src/seeder/fixtures/text-record.ts create mode 100644 packages/ens-test-kit/src/seeder/index.ts create mode 100644 packages/ens-test-kit/src/seeder/types.ts create mode 100644 packages/ens-test-kit/src/types/index.ts create mode 100644 packages/ens-test-kit/src/vitest/index.ts create mode 100644 packages/ens-test-kit/tsconfig.json create mode 100644 packages/ens-test-kit/tsup.config.ts create mode 100644 packages/ens-test-kit/vitest.config.ts diff --git a/.changeset/config.json b/.changeset/config.json index 64f132b826..e9e4ad191f 100644 --- a/.changeset/config.json +++ b/.changeset/config.json @@ -28,7 +28,8 @@ "@docs/ensrainbow", "@namehash/ens-referrals", "@namehash/namehash-ui", - "@ensnode/ensindexer-perf-testing" + "@ensnode/ensindexer-perf-testing", + "@ensnode/ens-test-kit" ] ], "updateInternalDependencies": "patch", diff --git a/packages/ens-test-kit/README.md b/packages/ens-test-kit/README.md new file mode 100644 index 0000000000..23d105e7d8 --- /dev/null +++ b/packages/ens-test-kit/README.md @@ -0,0 +1,13 @@ +# `@ensnode/ens-test-kit` + +Declarative ENS testing primitives and fixtures for ENSNode. + +This package currently ships the initial type contracts and module layout for: + +- API interfaces (`ResolutionsApi`, `DomainsApi`, `AccountsApi`, `ResolversApi`) +- Shared testing types (`Domain`, `Account`, `Resolver`, `Registration`, and related aliases) +- Case types (`TestCase`) +- Seeder fixture type contracts +- Vitest and CLI type entrypoints + +Runtime seeding and test-runner behavior will be added in follow-up steps. diff --git a/packages/ens-test-kit/package.json b/packages/ens-test-kit/package.json new file mode 100644 index 0000000000..33c2923f04 --- /dev/null +++ b/packages/ens-test-kit/package.json @@ -0,0 +1,85 @@ +{ + "name": "@ensnode/ens-test-kit", + "version": "1.10.1", + "type": "module", + "description": "Declarative ENS testing primitives and fixtures for ENSNode", + "license": "MIT", + "repository": { + "type": "git", + "url": "git+https://github.com/namehash/ensnode.git", + "directory": "packages/ens-test-kit" + }, + "homepage": "https://github.com/namehash/ensnode/tree/main/packages/ens-test-kit", + "keywords": [ + "ENS", + "ENSNode", + "testing" + ], + "files": [ + "dist" + ], + "exports": { + ".": "./src/index.ts", + "./interfaces": "./src/interfaces/index.ts", + "./types": "./src/types/index.ts", + "./cases": "./src/cases/index.ts", + "./seeder": "./src/seeder/index.ts", + "./vitest": "./src/vitest/index.ts", + "./cli": "./src/cli/index.ts" + }, + "sideEffects": false, + "publishConfig": { + "access": "public", + "exports": { + ".": { + "default": "./dist/index.js", + "types": "./dist/index.d.ts" + }, + "./interfaces": { + "default": "./dist/interfaces/index.js", + "types": "./dist/interfaces/index.d.ts" + }, + "./types": { + "default": "./dist/types/index.js", + "types": "./dist/types/index.d.ts" + }, + "./cases": { + "default": "./dist/cases/index.js", + "types": "./dist/cases/index.d.ts" + }, + "./seeder": { + "default": "./dist/seeder/index.js", + "types": "./dist/seeder/index.d.ts" + }, + "./vitest": { + "default": "./dist/vitest/index.js", + "types": "./dist/vitest/index.d.ts" + }, + "./cli": { + "default": "./dist/cli/index.js", + "types": "./dist/cli/index.d.ts" + } + }, + "main": "./dist/index.js", + "module": "./dist/index.mjs", + "types": "./dist/index.d.ts" + }, + "scripts": { + "prepublish": "tsup", + "lint": "biome check --write .", + "lint:ci": "biome ci", + "test": "vitest", + "typecheck": "tsgo --noEmit" + }, + "peerDependencies": { + "viem": "catalog:" + }, + "devDependencies": { + "@ensnode/shared-configs": "workspace:*", + "@types/node": "catalog:", + "tsup": "catalog:", + "typescript": "catalog:", + "viem": "catalog:", + "vitest": "catalog:" + } +} diff --git a/packages/ens-test-kit/src/cases/index.ts b/packages/ens-test-kit/src/cases/index.ts new file mode 100644 index 0000000000..f099478e18 --- /dev/null +++ b/packages/ens-test-kit/src/cases/index.ts @@ -0,0 +1 @@ +export type { TestCase } from "./types"; diff --git a/packages/ens-test-kit/src/cases/types.ts b/packages/ens-test-kit/src/cases/types.ts new file mode 100644 index 0000000000..19cacd5503 --- /dev/null +++ b/packages/ens-test-kit/src/cases/types.ts @@ -0,0 +1,9 @@ +import type { Fixture } from "../seeder/types"; + +export interface TestCase { + id: string; + description: string; + fixtures: Fixture[]; + call: (api: Api) => Promise; + expected: unknown; +} diff --git a/packages/ens-test-kit/src/cli/index.ts b/packages/ens-test-kit/src/cli/index.ts new file mode 100644 index 0000000000..e4f42e4191 --- /dev/null +++ b/packages/ens-test-kit/src/cli/index.ts @@ -0,0 +1,4 @@ +export interface SeedCliOptions { + rpcUrl: string; + fixtures?: string; +} diff --git a/packages/ens-test-kit/src/index.ts b/packages/ens-test-kit/src/index.ts new file mode 100644 index 0000000000..10a0c65af8 --- /dev/null +++ b/packages/ens-test-kit/src/index.ts @@ -0,0 +1,6 @@ +export type * from "./cases"; +export type * from "./cli"; +export type * from "./interfaces"; +export type * from "./seeder"; +export type * from "./types"; +export type * from "./vitest"; diff --git a/packages/ens-test-kit/src/interfaces/accounts.ts b/packages/ens-test-kit/src/interfaces/accounts.ts new file mode 100644 index 0000000000..04fc338436 --- /dev/null +++ b/packages/ens-test-kit/src/interfaces/accounts.ts @@ -0,0 +1,5 @@ +import type { Account, Hex } from "../types"; + +export interface AccountsApi { + getAccount(address: Hex): Promise; +} diff --git a/packages/ens-test-kit/src/interfaces/domains.ts b/packages/ens-test-kit/src/interfaces/domains.ts new file mode 100644 index 0000000000..89b4b8cc91 --- /dev/null +++ b/packages/ens-test-kit/src/interfaces/domains.ts @@ -0,0 +1,8 @@ +import type { Connection, Domain, DomainsWhere, Hex, NormalizedName, Registration } from "../types"; + +export interface DomainsApi { + getDomainByName(name: NormalizedName): Promise; + getDomainByNamehash(node: Hex): Promise; + listDomains(where: DomainsWhere): Promise>; + getRegistration(name: NormalizedName): Promise; +} diff --git a/packages/ens-test-kit/src/interfaces/index.ts b/packages/ens-test-kit/src/interfaces/index.ts new file mode 100644 index 0000000000..690be42593 --- /dev/null +++ b/packages/ens-test-kit/src/interfaces/index.ts @@ -0,0 +1,4 @@ +export type { AccountsApi } from "./accounts"; +export type { DomainsApi } from "./domains"; +export type { ResolutionsApi } from "./resolutions"; +export type { ResolversApi } from "./resolvers"; diff --git a/packages/ens-test-kit/src/interfaces/resolutions.ts b/packages/ens-test-kit/src/interfaces/resolutions.ts new file mode 100644 index 0000000000..b25e94b606 --- /dev/null +++ b/packages/ens-test-kit/src/interfaces/resolutions.ts @@ -0,0 +1,7 @@ +import type { ChainId, Hex, NormalizedName, RecordsSelection, ResolvedRecords } from "../types"; + +export interface ResolutionsApi { + resolveRecords(name: NormalizedName, selection: RecordsSelection): Promise; + resolvePrimaryName(address: Hex, chainId: ChainId): Promise; + resolvePrimaryNames(address: Hex, chainIds?: ChainId[]): Promise>; +} diff --git a/packages/ens-test-kit/src/interfaces/resolvers.ts b/packages/ens-test-kit/src/interfaces/resolvers.ts new file mode 100644 index 0000000000..0978541891 --- /dev/null +++ b/packages/ens-test-kit/src/interfaces/resolvers.ts @@ -0,0 +1,6 @@ +import type { ChainId, NormalizedName, Resolver, ResolverId } from "../types"; + +export interface ResolversApi { + getResolver(id: ResolverId): Promise; + listResolverRecords(name: NormalizedName): Promise<{ keys: string[]; coinTypes: ChainId[] }>; +} diff --git a/packages/ens-test-kit/src/seeder/fixtures/abi.ts b/packages/ens-test-kit/src/seeder/fixtures/abi.ts new file mode 100644 index 0000000000..7154806a4d --- /dev/null +++ b/packages/ens-test-kit/src/seeder/fixtures/abi.ts @@ -0,0 +1,10 @@ +import type { Hex, NormalizedName } from "../../types"; + +export interface AbiFixture { + kind: "abi"; + id: string; + name: NormalizedName; + resolverAddress?: Hex; + contentType: number; + value: string; +} diff --git a/packages/ens-test-kit/src/seeder/fixtures/contenthash.ts b/packages/ens-test-kit/src/seeder/fixtures/contenthash.ts new file mode 100644 index 0000000000..2552dcd144 --- /dev/null +++ b/packages/ens-test-kit/src/seeder/fixtures/contenthash.ts @@ -0,0 +1,9 @@ +import type { Hex, NormalizedName } from "../../types"; + +export interface ContenthashFixture { + kind: "contenthash"; + id: string; + name: NormalizedName; + resolverAddress?: Hex; + value: string; +} diff --git a/packages/ens-test-kit/src/seeder/fixtures/interface-implementer.ts b/packages/ens-test-kit/src/seeder/fixtures/interface-implementer.ts new file mode 100644 index 0000000000..8d193aaeb8 --- /dev/null +++ b/packages/ens-test-kit/src/seeder/fixtures/interface-implementer.ts @@ -0,0 +1,10 @@ +import type { Hex, NormalizedName } from "../../types"; + +export interface InterfaceImplementerFixture { + kind: "interface-implementer"; + id: string; + name: NormalizedName; + resolverAddress?: Hex; + interfaceId: Hex; + implementer: Hex; +} diff --git a/packages/ens-test-kit/src/seeder/fixtures/multicoin-address.ts b/packages/ens-test-kit/src/seeder/fixtures/multicoin-address.ts new file mode 100644 index 0000000000..cb89188d63 --- /dev/null +++ b/packages/ens-test-kit/src/seeder/fixtures/multicoin-address.ts @@ -0,0 +1,10 @@ +import type { ChainId, Hex, NormalizedName } from "../../types"; + +export interface MulticoinAddressFixture { + kind: "multicoin-address"; + id: string; + name: NormalizedName; + resolverAddress?: Hex; + coinType: ChainId; + value: string; +} diff --git a/packages/ens-test-kit/src/seeder/fixtures/primary-name.ts b/packages/ens-test-kit/src/seeder/fixtures/primary-name.ts new file mode 100644 index 0000000000..f26dea02e1 --- /dev/null +++ b/packages/ens-test-kit/src/seeder/fixtures/primary-name.ts @@ -0,0 +1,9 @@ +import type { ChainId, Hex, NormalizedName } from "../../types"; + +export interface PrimaryNameFixture { + kind: "primary-name"; + id: string; + address: Hex; + chainId: ChainId; + name: NormalizedName; +} diff --git a/packages/ens-test-kit/src/seeder/fixtures/pubkey.ts b/packages/ens-test-kit/src/seeder/fixtures/pubkey.ts new file mode 100644 index 0000000000..87765d6320 --- /dev/null +++ b/packages/ens-test-kit/src/seeder/fixtures/pubkey.ts @@ -0,0 +1,10 @@ +import type { Hex, NormalizedName } from "../../types"; + +export interface PubkeyFixture { + kind: "pubkey"; + id: string; + name: NormalizedName; + resolverAddress?: Hex; + x: Hex; + y: Hex; +} diff --git a/packages/ens-test-kit/src/seeder/fixtures/text-record.ts b/packages/ens-test-kit/src/seeder/fixtures/text-record.ts new file mode 100644 index 0000000000..7501d22b82 --- /dev/null +++ b/packages/ens-test-kit/src/seeder/fixtures/text-record.ts @@ -0,0 +1,10 @@ +import type { Hex, NormalizedName } from "../../types"; + +export interface TextRecordFixture { + kind: "text-record"; + id: string; + name: NormalizedName; + resolverAddress?: Hex; + key: string; + value: string; +} diff --git a/packages/ens-test-kit/src/seeder/index.ts b/packages/ens-test-kit/src/seeder/index.ts new file mode 100644 index 0000000000..b4fa7779ce --- /dev/null +++ b/packages/ens-test-kit/src/seeder/index.ts @@ -0,0 +1,8 @@ +export type { AbiFixture } from "./fixtures/abi"; +export type { ContenthashFixture } from "./fixtures/contenthash"; +export type { InterfaceImplementerFixture } from "./fixtures/interface-implementer"; +export type { MulticoinAddressFixture } from "./fixtures/multicoin-address"; +export type { PrimaryNameFixture } from "./fixtures/primary-name"; +export type { PubkeyFixture } from "./fixtures/pubkey"; +export type { TextRecordFixture } from "./fixtures/text-record"; +export type { Fixture, FixtureBase, FixtureKind, SeederContext } from "./types"; diff --git a/packages/ens-test-kit/src/seeder/types.ts b/packages/ens-test-kit/src/seeder/types.ts new file mode 100644 index 0000000000..ea4111728b --- /dev/null +++ b/packages/ens-test-kit/src/seeder/types.ts @@ -0,0 +1,29 @@ +import type { AbiFixture } from "./fixtures/abi"; +import type { ContenthashFixture } from "./fixtures/contenthash"; +import type { InterfaceImplementerFixture } from "./fixtures/interface-implementer"; +import type { MulticoinAddressFixture } from "./fixtures/multicoin-address"; +import type { PrimaryNameFixture } from "./fixtures/primary-name"; +import type { PubkeyFixture } from "./fixtures/pubkey"; +import type { TextRecordFixture } from "./fixtures/text-record"; + +export type Fixture = + | PrimaryNameFixture + | TextRecordFixture + | MulticoinAddressFixture + | ContenthashFixture + | PubkeyFixture + | AbiFixture + | InterfaceImplementerFixture; + +export type FixtureKind = Fixture["kind"]; +export type FixtureBase = Pick; + +export interface SeederContext { + rpcUrl: string; + clients: { + deployer: unknown; + owner: unknown; + user: unknown; + user2: unknown; + }; +} diff --git a/packages/ens-test-kit/src/types/index.ts b/packages/ens-test-kit/src/types/index.ts new file mode 100644 index 0000000000..8709643c98 --- /dev/null +++ b/packages/ens-test-kit/src/types/index.ts @@ -0,0 +1,69 @@ +import type { Hex as ViemHex } from "viem"; + +export type Hex = ViemHex; +export type ChainId = number; +export type NormalizedName = string; +export type ResolverId = string; + +export interface Registration { + name: NormalizedName; + registrant: Hex | null; + registrationDate: string | null; + expiryDate: string | null; +} + +export interface Domain { + name: NormalizedName; + namehash: Hex; + owner: Hex | null; + resolverId: ResolverId | null; + registration: Registration | null; + parentName: NormalizedName | null; +} + +export interface Account { + address: Hex; + domains: Domain[]; + primaryNames?: Partial>; +} + +export interface Resolver { + id: ResolverId; + address: Hex; + domainNames: NormalizedName[]; +} + +export interface Connection { + items: T[]; + totalCount: number; +} + +export interface DomainsWhere { + owner?: Hex; + parentName?: NormalizedName; + resolverId?: ResolverId; + nameContains?: string; +} + +export interface RecordsSelection { + name?: boolean; + addresses?: ChainId[]; + texts?: string[]; + contenthash?: boolean; + pubkey?: boolean; + abi?: boolean; + interfaceIds?: Hex[]; +} + +export interface ResolvedRecords { + name?: NormalizedName | null; + addresses?: Partial>; + texts?: Record; + contenthash?: string | null; + pubkey?: { + x: Hex; + y: Hex; + } | null; + abi?: string | null; + interfaces?: Record; +} diff --git a/packages/ens-test-kit/src/vitest/index.ts b/packages/ens-test-kit/src/vitest/index.ts new file mode 100644 index 0000000000..348bcdf617 --- /dev/null +++ b/packages/ens-test-kit/src/vitest/index.ts @@ -0,0 +1,3 @@ +import type { TestCase } from "../cases/types"; + +export type RunSuite = (adapter: Api, cases: TestCase[]) => void; diff --git a/packages/ens-test-kit/tsconfig.json b/packages/ens-test-kit/tsconfig.json new file mode 100644 index 0000000000..a33791a208 --- /dev/null +++ b/packages/ens-test-kit/tsconfig.json @@ -0,0 +1,5 @@ +{ + "extends": "@ensnode/shared-configs/tsconfig.lib.json", + "include": ["src/**/*"], + "exclude": ["dist"] +} diff --git a/packages/ens-test-kit/tsup.config.ts b/packages/ens-test-kit/tsup.config.ts new file mode 100644 index 0000000000..d58f21a3fd --- /dev/null +++ b/packages/ens-test-kit/tsup.config.ts @@ -0,0 +1,23 @@ +import { defineConfig } from "tsup"; + +export default defineConfig({ + entry: { + index: "src/index.ts", + "interfaces/index": "src/interfaces/index.ts", + "types/index": "src/types/index.ts", + "cases/index": "src/cases/index.ts", + "seeder/index": "src/seeder/index.ts", + "vitest/index": "src/vitest/index.ts", + "cli/index": "src/cli/index.ts", + }, + platform: "neutral", + format: ["esm"], + target: "es2022", + bundle: true, + splitting: false, + sourcemap: true, + dts: true, + clean: true, + external: ["viem"], + outDir: "./dist", +}); diff --git a/packages/ens-test-kit/vitest.config.ts b/packages/ens-test-kit/vitest.config.ts new file mode 100644 index 0000000000..ce487d95b8 --- /dev/null +++ b/packages/ens-test-kit/vitest.config.ts @@ -0,0 +1,8 @@ +import { configDefaults, defineConfig } from "vitest/config"; + +export default defineConfig({ + test: { + environment: "node", + exclude: [...configDefaults.exclude, "**/*.integration.test.ts"], + }, +}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 26aae024b4..1a4b7caafb 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -900,6 +900,27 @@ importers: specifier: 'catalog:' version: 4.0.5(@types/debug@4.1.12)(@types/node@24.10.9)(jiti@2.6.1)(jsdom@27.0.1(postcss@8.5.12))(lightningcss@1.30.2)(tsx@4.21.0)(yaml@2.8.3) + packages/ens-test-kit: + devDependencies: + '@ensnode/shared-configs': + specifier: workspace:* + version: link:../shared-configs + '@types/node': + specifier: 'catalog:' + version: 24.10.9 + tsup: + specifier: 'catalog:' + version: 8.5.0(jiti@2.6.1)(postcss@8.5.6)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3) + typescript: + specifier: 'catalog:' + version: 5.9.3 + viem: + specifier: 'catalog:' + version: 2.38.5(typescript@5.9.3)(zod@4.3.6) + vitest: + specifier: 'catalog:' + version: 4.0.5(@types/debug@4.1.12)(@types/node@24.10.9)(jiti@2.6.1)(jsdom@27.0.1(postcss@8.5.6))(lightningcss@1.30.2)(tsx@4.21.0)(yaml@2.8.3) + packages/enscli: {} packages/ensdb-sdk: From f88184f0de61da34de4eeaee4ac5cb893d593983 Mon Sep 17 00:00:00 2001 From: sevenzing Date: Fri, 1 May 2026 16:38:39 +0400 Subject: [PATCH 15/18] docs(changeset): Add new ens-test-kit package - tool to describe ens test cases --- .changeset/tidy-states-sell.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/tidy-states-sell.md diff --git a/.changeset/tidy-states-sell.md b/.changeset/tidy-states-sell.md new file mode 100644 index 0000000000..62d3bdf105 --- /dev/null +++ b/.changeset/tidy-states-sell.md @@ -0,0 +1,5 @@ +--- +"@ensnode/ens-test-kit": patch +--- + +Add new ens-test-kit package - tool to describe ens test cases From 74d1306327b6a8f92fdd8654fbe05f7515f84819 Mon Sep 17 00:00:00 2001 From: sevenzing Date: Fri, 1 May 2026 18:43:48 +0400 Subject: [PATCH 16/18] add fixture implementations --- docker/services/devnet.yml | 13 +- packages/ens-test-kit/bin/ens-test-kit.mjs | 9 + packages/ens-test-kit/devnet/Dockerfile | 37 ++++ packages/ens-test-kit/devnet/entrypoint.sh | 58 +++++++ packages/ens-test-kit/package.json | 10 +- packages/ens-test-kit/src/cli/index.ts | 2 + packages/ens-test-kit/src/cli/seed.ts | 55 ++++++ .../ens-test-kit/src/seeder/fixtures/abi.ts | 40 ++++- .../ens-test-kit/src/seeder/fixtures/base.ts | 28 +++ .../src/seeder/fixtures/common.ts | 90 ++++++++++ .../src/seeder/fixtures/contenthash.ts | 44 ++++- .../seeder/fixtures/interface-implementer.ts | 10 -- .../src/seeder/fixtures/interface-record.ts | 38 ++++ .../src/seeder/fixtures/multicoin-address.ts | 43 ++++- .../src/seeder/fixtures/primary-name.ts | 9 - .../src/seeder/fixtures/pubkey.ts | 40 ++++- .../src/seeder/fixtures/resolver-utils.ts | 63 +++++++ .../src/seeder/fixtures/reverse-name.ts | 45 +++++ .../src/seeder/fixtures/sender-client.ts | 8 + .../src/seeder/fixtures/text-record.ts | 37 +++- packages/ens-test-kit/src/seeder/index.ts | 29 +++- packages/ens-test-kit/src/seeder/runtime.ts | 130 ++++++++++++++ .../ens-test-kit/src/seeder/tx-receipts.ts | 19 ++ packages/ens-test-kit/src/seeder/types.ts | 46 +++-- packages/ens-test-kit/tsup.config.ts | 1 + .../integration-test-env/src/orchestrator.ts | 17 +- .../integration-test-env/src/seed/index.ts | 68 -------- .../src/seed/primary-names.ts | 20 --- .../src/seed/resolver-records.ts | 163 ------------------ pnpm-lock.yaml | 13 +- 30 files changed, 831 insertions(+), 354 deletions(-) create mode 100644 packages/ens-test-kit/bin/ens-test-kit.mjs create mode 100644 packages/ens-test-kit/devnet/Dockerfile create mode 100644 packages/ens-test-kit/devnet/entrypoint.sh create mode 100644 packages/ens-test-kit/src/cli/seed.ts create mode 100644 packages/ens-test-kit/src/seeder/fixtures/base.ts create mode 100644 packages/ens-test-kit/src/seeder/fixtures/common.ts delete mode 100644 packages/ens-test-kit/src/seeder/fixtures/interface-implementer.ts create mode 100644 packages/ens-test-kit/src/seeder/fixtures/interface-record.ts delete mode 100644 packages/ens-test-kit/src/seeder/fixtures/primary-name.ts create mode 100644 packages/ens-test-kit/src/seeder/fixtures/resolver-utils.ts create mode 100644 packages/ens-test-kit/src/seeder/fixtures/reverse-name.ts create mode 100644 packages/ens-test-kit/src/seeder/fixtures/sender-client.ts create mode 100644 packages/ens-test-kit/src/seeder/runtime.ts create mode 100644 packages/ens-test-kit/src/seeder/tx-receipts.ts delete mode 100644 packages/integration-test-env/src/seed/index.ts delete mode 100644 packages/integration-test-env/src/seed/primary-names.ts delete mode 100644 packages/integration-test-env/src/seed/resolver-records.ts diff --git a/docker/services/devnet.yml b/docker/services/devnet.yml index 612394c549..f51260e2ba 100644 --- a/docker/services/devnet.yml +++ b/docker/services/devnet.yml @@ -1,15 +1,20 @@ services: devnet: container_name: devnet - image: ghcr.io/ensdomains/contracts-v2:main-9f26a8f - command: ./script/runDevnet.ts --testNames - pull_policy: always + build: + context: ../.. + dockerfile: packages/ens-test-kit/devnet/Dockerfile ports: - "8545:8545" environment: ANVIL_IP_ADDR: "0.0.0.0" + SEED_SENTINEL_PATH: "/tmp/ens-test-kit-seeded" healthcheck: - test: [ "CMD", "curl", "--fail", "-s", "http://localhost:8000/health" ] + test: + [ + "CMD-SHELL", + "test -f /tmp/ens-test-kit-seeded && curl --fail -s http://localhost:8000/health", + ] interval: 10s timeout: 5s retries: 5 diff --git a/packages/ens-test-kit/bin/ens-test-kit.mjs b/packages/ens-test-kit/bin/ens-test-kit.mjs new file mode 100644 index 0000000000..ad064d51e0 --- /dev/null +++ b/packages/ens-test-kit/bin/ens-test-kit.mjs @@ -0,0 +1,9 @@ +#!/usr/bin/env node + +import("../dist/cli/seed.js") + .then(({ runSeedCli }) => runSeedCli(process.argv.slice(2))) + .catch((error) => { + const message = error instanceof Error ? error.message : String(error); + console.error(`[ens-test-kit] ${message}`); + process.exit(1); + }); diff --git a/packages/ens-test-kit/devnet/Dockerfile b/packages/ens-test-kit/devnet/Dockerfile new file mode 100644 index 0000000000..6ed24b7aeb --- /dev/null +++ b/packages/ens-test-kit/devnet/Dockerfile @@ -0,0 +1,37 @@ +FROM node:24-slim AS build +ENV PNPM_HOME="/pnpm" +ENV PATH="$PNPM_HOME:$PATH" +RUN corepack enable +WORKDIR /workspace + +COPY pnpm-lock.yaml pnpm-workspace.yaml package.json ./ +COPY patches ./patches +COPY packages/ens-test-kit ./packages/ens-test-kit +COPY packages/datasources ./packages/datasources +COPY packages/enssdk ./packages/enssdk +COPY packages/shared-configs ./packages/shared-configs + +RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm install --frozen-lockfile +RUN pnpm -F @ensnode/ens-test-kit build + +FROM ghcr.io/ensdomains/contracts-v2:main-9f26a8f AS runtime + +RUN apt-get update \ + && apt-get install -y --no-install-recommends ca-certificates curl nodejs \ + && rm -rf /var/lib/apt/lists/* + +WORKDIR /app/contracts + +COPY --from=build /workspace/node_modules /workspace/node_modules +COPY --from=build /workspace/packages/ens-test-kit /workspace/packages/ens-test-kit +COPY --from=build /workspace/packages/datasources /workspace/packages/datasources +COPY --from=build /workspace/packages/enssdk /workspace/packages/enssdk +COPY packages/ens-test-kit/devnet/entrypoint.sh /usr/local/bin/ens-test-kit-entrypoint.sh + +RUN chmod +x /usr/local/bin/ens-test-kit-entrypoint.sh + +ENV ANVIL_IP_ADDR=0.0.0.0 +ENV DEVNET_RPC_URL=http://localhost:8545 +ENV SEED_SENTINEL_PATH=/tmp/ens-test-kit-seeded + +ENTRYPOINT ["/usr/local/bin/ens-test-kit-entrypoint.sh"] diff --git a/packages/ens-test-kit/devnet/entrypoint.sh b/packages/ens-test-kit/devnet/entrypoint.sh new file mode 100644 index 0000000000..570415c9c6 --- /dev/null +++ b/packages/ens-test-kit/devnet/entrypoint.sh @@ -0,0 +1,58 @@ +#!/usr/bin/env bash +set -euo pipefail + +SEED_SENTINEL_PATH="${SEED_SENTINEL_PATH:-/tmp/ens-test-kit-seeded}" +DEVNET_RPC_URL="${DEVNET_RPC_URL:-http://localhost:8545}" + +rm -f "$SEED_SENTINEL_PATH" + +bun ./script/runDevnet.ts --testNames & +DEVNET_PID=$! + +cleanup() { + if kill -0 "$DEVNET_PID" 2>/dev/null; then + kill "$DEVNET_PID" || true + fi +} + +trap cleanup INT TERM + +echo "[entrypoint] Waiting for Anvil JSON-RPC at ${DEVNET_RPC_URL}..." +ANVIL_READY=0 +for _ in $(seq 1 120); do + if curl -s -X POST "$DEVNET_RPC_URL" \ + -H "Content-Type: application/json" \ + -d '{"jsonrpc":"2.0","method":"eth_blockNumber","params":[],"id":1}' \ + >/dev/null; then + ANVIL_READY=1 + break + fi + sleep 1 +done + +if [[ "$ANVIL_READY" -ne 1 ]]; then + echo "[entrypoint] Anvil did not become ready in time" >&2 + exit 1 +fi + +echo "[entrypoint] Waiting for contracts-v2 health endpoint..." +DEVNET_HEALTHY=0 +for _ in $(seq 1 120); do + if curl --fail -s http://localhost:8000/health >/dev/null; then + DEVNET_HEALTHY=1 + break + fi + sleep 1 +done + +if [[ "$DEVNET_HEALTHY" -ne 1 ]]; then + echo "[entrypoint] contracts-v2 health endpoint did not become ready in time" >&2 + exit 1 +fi + +echo "[entrypoint] Seeding devnet..." +bun /workspace/packages/ens-test-kit/bin/ens-test-kit.mjs seed --rpc "$DEVNET_RPC_URL" --fixtures canonical +touch "$SEED_SENTINEL_PATH" +echo "[entrypoint] Devnet seeded" + +wait "$DEVNET_PID" diff --git a/packages/ens-test-kit/package.json b/packages/ens-test-kit/package.json index 33c2923f04..5139db2caf 100644 --- a/packages/ens-test-kit/package.json +++ b/packages/ens-test-kit/package.json @@ -16,8 +16,12 @@ "testing" ], "files": [ - "dist" + "dist", + "bin" ], + "bin": { + "ens-test-kit": "./bin/ens-test-kit.mjs" + }, "exports": { ".": "./src/index.ts", "./interfaces": "./src/interfaces/index.ts", @@ -65,13 +69,15 @@ "types": "./dist/index.d.ts" }, "scripts": { + "build": "tsup", "prepublish": "tsup", "lint": "biome check --write .", "lint:ci": "biome ci", "test": "vitest", "typecheck": "tsgo --noEmit" }, - "peerDependencies": { + "dependencies": { + "@ensnode/datasources": "workspace:*", "viem": "catalog:" }, "devDependencies": { diff --git a/packages/ens-test-kit/src/cli/index.ts b/packages/ens-test-kit/src/cli/index.ts index e4f42e4191..7e69970084 100644 --- a/packages/ens-test-kit/src/cli/index.ts +++ b/packages/ens-test-kit/src/cli/index.ts @@ -2,3 +2,5 @@ export interface SeedCliOptions { rpcUrl: string; fixtures?: string; } + +export { runSeedCli } from "./seed"; diff --git a/packages/ens-test-kit/src/cli/seed.ts b/packages/ens-test-kit/src/cli/seed.ts new file mode 100644 index 0000000000..0790cfa951 --- /dev/null +++ b/packages/ens-test-kit/src/cli/seed.ts @@ -0,0 +1,55 @@ +import { parseArgs } from "node:util"; + +import { getFixtureSet, seedFixtures } from "../seeder"; + +type ParsedArgs = { + rpcUrl: string; + fixtureSet: string; +}; + +function parseSeedArgs(argv: string[]): ParsedArgs { + const [subcommand, ...rest] = argv; + if (subcommand !== "seed") { + throw new Error(`Unknown command "${subcommand ?? ""}". Expected: seed.`); + } + + const { values } = parseArgs({ + args: rest, + options: { + rpc: { + type: "string", + }, + fixtures: { + type: "string", + default: "canonical", + }, + }, + strict: true, + allowPositionals: false, + }); + + if (!values.rpc) { + throw new Error("Missing required argument: --rpc ."); + } + + return { + rpcUrl: values.rpc, + fixtureSet: values.fixtures, + }; +} + +export async function runSeedCli(argv: string[]): Promise { + const { rpcUrl, fixtureSet } = parseSeedArgs(argv); + const fixtures = getFixtureSet(fixtureSet); + const applied = await seedFixtures(rpcUrl, fixtures); + + const names = [ + ...new Set( + applied.map((fixture) => ("name" in fixture ? fixture.name : undefined)).filter(Boolean), + ), + ]; + console.log(`[seed] Applied ${applied.length} fixture(s) from set "${fixtureSet}".`); + if (names.length > 0) { + console.log(`[seed] Names: ${names.join(", ")}`); + } +} diff --git a/packages/ens-test-kit/src/seeder/fixtures/abi.ts b/packages/ens-test-kit/src/seeder/fixtures/abi.ts index 7154806a4d..b11c478528 100644 --- a/packages/ens-test-kit/src/seeder/fixtures/abi.ts +++ b/packages/ens-test-kit/src/seeder/fixtures/abi.ts @@ -1,10 +1,36 @@ -import type { Hex, NormalizedName } from "../../types"; +import type { Hex } from "../../types"; +import type { SeederContext } from "../types"; +import { + buildFixture, + type FixtureArgs, + type FixtureMeta, + type ResolverRecordFields, +} from "./base"; +import { assertExpectedResolver, getResolverAndNode, writeResolverTx } from "./resolver-utils"; +import { getSenderClient } from "./sender-client"; -export interface AbiFixture { - kind: "abi"; - id: string; - name: NormalizedName; - resolverAddress?: Hex; +type AbiRecordFields = ResolverRecordFields & { contentType: number; - value: string; + value: Hex; +}; + +export type AbiRecordFixture = FixtureMeta<"abiRecord"> & AbiRecordFields; + +export function abiRecord(args: FixtureArgs<"abiRecord", AbiRecordFields>): AbiRecordFixture { + return buildFixture("abiRecord", args); +} + +export async function applyAbiRecordFixture( + fixture: AbiRecordFixture, + ctx: SeederContext, +): Promise { + const senderClient = getSenderClient(ctx, fixture.sender); + const { node, resolver } = getResolverAndNode(fixture.name, fixture.resolverAddress); + await assertExpectedResolver(senderClient, fixture.name, resolver); + await writeResolverTx(senderClient, { + resolver, + functionName: "setABI", + data: [node, BigInt(fixture.contentType), fixture.value], + log: `abi ${fixture.name} contentType=${fixture.contentType}`, + }); } diff --git a/packages/ens-test-kit/src/seeder/fixtures/base.ts b/packages/ens-test-kit/src/seeder/fixtures/base.ts new file mode 100644 index 0000000000..6803ddbb55 --- /dev/null +++ b/packages/ens-test-kit/src/seeder/fixtures/base.ts @@ -0,0 +1,28 @@ +import type { Hex, NormalizedName } from "../../types"; +import type { SeederSender } from "../types"; + +export type FixtureMeta = { + kind: K; + id: string; + sender?: SeederSender; +}; + +export type FixtureArgs = Omit< + FixtureMeta & Fields, + "kind" +>; + +export function buildFixture( + kind: K, + args: FixtureArgs, +): FixtureMeta & Fields { + return { + kind, + ...args, + } as FixtureMeta & Fields; +} + +export type ResolverRecordFields = { + name: NormalizedName; + resolverAddress?: Hex; +}; diff --git a/packages/ens-test-kit/src/seeder/fixtures/common.ts b/packages/ens-test-kit/src/seeder/fixtures/common.ts new file mode 100644 index 0000000000..796c8f7432 --- /dev/null +++ b/packages/ens-test-kit/src/seeder/fixtures/common.ts @@ -0,0 +1,90 @@ +import { accounts, addresses, fixtures } from "@ensnode/datasources/devnet"; + +import type { Fixture } from "../types"; +import { abiRecord } from "./abi"; +import { contenthashRecord } from "./contenthash"; +import { interfaceRecord } from "./interface-record"; +import { multicoinAddressRecord } from "./multicoin-address"; +import { pubkeyRecord } from "./pubkey"; +import { reverseName } from "./reverse-name"; +import { textRecord } from "./text-record"; + +export const canonicalFixtures: Fixture[] = [ + reverseName({ + id: "reverse-name-owner-test-eth", + address: accounts.owner.address, + chainId: 1, + name: "test.eth", + }), + textRecord({ + id: "text-record-test-eth-avatar", + name: "test.eth", + key: "avatar", + value: "https://example.com/avatar.png", + }), + textRecord({ + id: "text-record-test-eth-com-twitter", + name: "test.eth", + key: "com.twitter", + value: "ensdomains", + }), + textRecord({ + id: "text-record-test-eth-com-github", + name: "test.eth", + key: "com.github", + value: "ensdomains", + }), + textRecord({ + id: "text-record-test-eth-url", + name: "test.eth", + key: "url", + value: "https://ens.domains", + }), + textRecord({ + id: "text-record-test-eth-email", + name: "test.eth", + key: "email", + value: "test@ens.domains", + }), + textRecord({ + id: "text-record-test-eth-description", + name: "test.eth", + key: "description", + value: "test.eth", + }), + multicoinAddressRecord({ + id: "multicoin-address-test-eth-0", + name: "test.eth", + coinType: 0, + value: fixtures.bitcoinAddress, + }), + multicoinAddressRecord({ + id: "multicoin-address-test-eth-2", + name: "test.eth", + coinType: 2, + value: fixtures.litecoinAddress, + }), + contenthashRecord({ + id: "contenthash-test-eth", + name: "test.eth", + value: fixtures.contenthash, + }), + pubkeyRecord({ + id: "pubkey-test-eth", + name: "test.eth", + x: fixtures.publicKeyX, + y: fixtures.publicKeyY, + }), + abiRecord({ + id: "abi-test-eth-content-type-1", + name: "test.eth", + contentType: 1, + value: fixtures.abiBytes, + }), + interfaceRecord({ + id: "interface-record-test-eth-0x11100111", + name: "test.eth", + interfaceId: fixtures.fourBytesInterface, + value: addresses.one, + }), +]; diff --git a/packages/ens-test-kit/src/seeder/fixtures/contenthash.ts b/packages/ens-test-kit/src/seeder/fixtures/contenthash.ts index 2552dcd144..46819d558f 100644 --- a/packages/ens-test-kit/src/seeder/fixtures/contenthash.ts +++ b/packages/ens-test-kit/src/seeder/fixtures/contenthash.ts @@ -1,9 +1,37 @@ -import type { Hex, NormalizedName } from "../../types"; - -export interface ContenthashFixture { - kind: "contenthash"; - id: string; - name: NormalizedName; - resolverAddress?: Hex; - value: string; +import type { Hex } from "../../types"; +import type { SeederContext } from "../types"; +import { + buildFixture, + type FixtureArgs, + type FixtureMeta, + type ResolverRecordFields, +} from "./base"; +import { assertExpectedResolver, getResolverAndNode, writeResolverTx } from "./resolver-utils"; +import { getSenderClient } from "./sender-client"; + +type ContenthashRecordFields = ResolverRecordFields & { + value: Hex; +}; + +export type ContenthashRecordFixture = FixtureMeta<"contenthashRecord"> & ContenthashRecordFields; + +export function contenthashRecord( + args: FixtureArgs<"contenthashRecord", ContenthashRecordFields>, +): ContenthashRecordFixture { + return buildFixture("contenthashRecord", args); +} + +export async function applyContenthashRecordFixture( + fixture: ContenthashRecordFixture, + ctx: SeederContext, +): Promise { + const senderClient = getSenderClient(ctx, fixture.sender); + const { node, resolver } = getResolverAndNode(fixture.name, fixture.resolverAddress); + await assertExpectedResolver(senderClient, fixture.name, resolver); + await writeResolverTx(senderClient, { + resolver, + functionName: "setContenthash", + data: [node, fixture.value], + log: `contenthash ${fixture.name}`, + }); } diff --git a/packages/ens-test-kit/src/seeder/fixtures/interface-implementer.ts b/packages/ens-test-kit/src/seeder/fixtures/interface-implementer.ts deleted file mode 100644 index 8d193aaeb8..0000000000 --- a/packages/ens-test-kit/src/seeder/fixtures/interface-implementer.ts +++ /dev/null @@ -1,10 +0,0 @@ -import type { Hex, NormalizedName } from "../../types"; - -export interface InterfaceImplementerFixture { - kind: "interface-implementer"; - id: string; - name: NormalizedName; - resolverAddress?: Hex; - interfaceId: Hex; - implementer: Hex; -} diff --git a/packages/ens-test-kit/src/seeder/fixtures/interface-record.ts b/packages/ens-test-kit/src/seeder/fixtures/interface-record.ts new file mode 100644 index 0000000000..43e2ab9372 --- /dev/null +++ b/packages/ens-test-kit/src/seeder/fixtures/interface-record.ts @@ -0,0 +1,38 @@ +import type { Hex } from "../../types"; +import type { SeederContext } from "../types"; +import { + buildFixture, + type FixtureArgs, + type FixtureMeta, + type ResolverRecordFields, +} from "./base"; +import { assertExpectedResolver, getResolverAndNode, writeResolverTx } from "./resolver-utils"; +import { getSenderClient } from "./sender-client"; + +type InterfaceRecordFields = ResolverRecordFields & { + interfaceId: Hex; + value: Hex; +}; + +export type InterfaceRecordFixture = FixtureMeta<"interfaceRecord"> & InterfaceRecordFields; + +export function interfaceRecord( + args: FixtureArgs<"interfaceRecord", InterfaceRecordFields>, +): InterfaceRecordFixture { + return buildFixture("interfaceRecord", args); +} + +export async function applyInterfaceRecordFixture( + fixture: InterfaceRecordFixture, + ctx: SeederContext, +): Promise { + const senderClient = getSenderClient(ctx, fixture.sender); + const { node, resolver } = getResolverAndNode(fixture.name, fixture.resolverAddress); + await assertExpectedResolver(senderClient, fixture.name, resolver); + await writeResolverTx(senderClient, { + resolver, + functionName: "setInterface", + data: [node, fixture.interfaceId, fixture.value], + log: `interface-record ${fixture.name} interfaceId=${fixture.interfaceId}`, + }); +} diff --git a/packages/ens-test-kit/src/seeder/fixtures/multicoin-address.ts b/packages/ens-test-kit/src/seeder/fixtures/multicoin-address.ts index cb89188d63..485fa4f80c 100644 --- a/packages/ens-test-kit/src/seeder/fixtures/multicoin-address.ts +++ b/packages/ens-test-kit/src/seeder/fixtures/multicoin-address.ts @@ -1,10 +1,39 @@ -import type { ChainId, Hex, NormalizedName } from "../../types"; +import type { ChainId, Hex } from "../../types"; +import type { SeederContext } from "../types"; +import { + buildFixture, + type FixtureArgs, + type FixtureMeta, + type ResolverRecordFields, +} from "./base"; +import { assertExpectedResolver, getResolverAndNode, writeResolverTx } from "./resolver-utils"; +import { getSenderClient } from "./sender-client"; -export interface MulticoinAddressFixture { - kind: "multicoin-address"; - id: string; - name: NormalizedName; - resolverAddress?: Hex; +type MulticoinAddressRecordFields = ResolverRecordFields & { coinType: ChainId; - value: string; + value: Hex; +}; + +export type MulticoinAddressRecordFixture = FixtureMeta<"multicoinAddressRecord"> & + MulticoinAddressRecordFields; + +export function multicoinAddressRecord( + args: FixtureArgs<"multicoinAddressRecord", MulticoinAddressRecordFields>, +): MulticoinAddressRecordFixture { + return buildFixture("multicoinAddressRecord", args); +} + +export async function applyMulticoinAddressRecordFixture( + fixture: MulticoinAddressRecordFixture, + ctx: SeederContext, +): Promise { + const senderClient = getSenderClient(ctx, fixture.sender); + const { node, resolver } = getResolverAndNode(fixture.name, fixture.resolverAddress); + await assertExpectedResolver(senderClient, fixture.name, resolver); + await writeResolverTx(senderClient, { + resolver, + functionName: "setAddr", + data: [node, BigInt(fixture.coinType), fixture.value], + log: `multicoin-address ${fixture.name} coinType=${fixture.coinType}`, + }); } diff --git a/packages/ens-test-kit/src/seeder/fixtures/primary-name.ts b/packages/ens-test-kit/src/seeder/fixtures/primary-name.ts deleted file mode 100644 index f26dea02e1..0000000000 --- a/packages/ens-test-kit/src/seeder/fixtures/primary-name.ts +++ /dev/null @@ -1,9 +0,0 @@ -import type { ChainId, Hex, NormalizedName } from "../../types"; - -export interface PrimaryNameFixture { - kind: "primary-name"; - id: string; - address: Hex; - chainId: ChainId; - name: NormalizedName; -} diff --git a/packages/ens-test-kit/src/seeder/fixtures/pubkey.ts b/packages/ens-test-kit/src/seeder/fixtures/pubkey.ts index 87765d6320..d1c43c81e6 100644 --- a/packages/ens-test-kit/src/seeder/fixtures/pubkey.ts +++ b/packages/ens-test-kit/src/seeder/fixtures/pubkey.ts @@ -1,10 +1,38 @@ -import type { Hex, NormalizedName } from "../../types"; +import type { Hex } from "../../types"; +import type { SeederContext } from "../types"; +import { + buildFixture, + type FixtureArgs, + type FixtureMeta, + type ResolverRecordFields, +} from "./base"; +import { assertExpectedResolver, getResolverAndNode, writeResolverTx } from "./resolver-utils"; +import { getSenderClient } from "./sender-client"; -export interface PubkeyFixture { - kind: "pubkey"; - id: string; - name: NormalizedName; - resolverAddress?: Hex; +type PubkeyRecordFields = ResolverRecordFields & { x: Hex; y: Hex; +}; + +export type PubkeyRecordFixture = FixtureMeta<"pubkeyRecord"> & PubkeyRecordFields; + +export function pubkeyRecord( + args: FixtureArgs<"pubkeyRecord", PubkeyRecordFields>, +): PubkeyRecordFixture { + return buildFixture("pubkeyRecord", args); +} + +export async function applyPubkeyRecordFixture( + fixture: PubkeyRecordFixture, + ctx: SeederContext, +): Promise { + const senderClient = getSenderClient(ctx, fixture.sender); + const { node, resolver } = getResolverAndNode(fixture.name, fixture.resolverAddress); + await assertExpectedResolver(senderClient, fixture.name, resolver); + await writeResolverTx(senderClient, { + resolver, + functionName: "setPubkey", + data: [node, fixture.x, fixture.y], + log: `pubkey ${fixture.name}`, + }); } diff --git a/packages/ens-test-kit/src/seeder/fixtures/resolver-utils.ts b/packages/ens-test-kit/src/seeder/fixtures/resolver-utils.ts new file mode 100644 index 0000000000..fda271cc5d --- /dev/null +++ b/packages/ens-test-kit/src/seeder/fixtures/resolver-utils.ts @@ -0,0 +1,63 @@ +import type { Address, Hex } from "viem"; +import { namehash, toHex } from "viem"; +import { packetToBytes } from "viem/ens"; + +import { ResolverABI, UniversalResolverABI } from "@ensnode/datasources"; +import { contracts } from "@ensnode/datasources/devnet"; + +import { waitForTransactionReceipt } from "../tx-receipts"; +import type { DevnetWalletClient } from "../types"; + +export function getResolverAndNode( + name: string, + resolverAddress?: Hex, +): { node: Hex; resolver: Address } { + return { + node: namehash(name), + resolver: (resolverAddress ?? contracts.permissionedResolver) as Address, + }; +} + +export async function assertExpectedResolver( + client: DevnetWalletClient, + name: string, + expectedResolver: Address, +): Promise { + const [actualResolver] = await client.readContract({ + address: contracts.universalResolverV2, + abi: UniversalResolverABI, + functionName: "findResolver", + args: [toHex(packetToBytes(name))], + }); + + if (actualResolver.toLowerCase() !== expectedResolver.toLowerCase()) { + throw new Error( + `${name} resolver mismatch: active=${actualResolver}, expected=${expectedResolver}.`, + ); + } +} + +export async function writeResolverTx( + client: DevnetWalletClient, + args: { + resolver: Address; + functionName: + | "setText" + | "setAddr" + | "setContenthash" + | "setPubkey" + | "setABI" + | "setInterface"; + data: unknown[]; + log: string; + }, +): Promise { + const hash = await client.writeContract({ + address: args.resolver, + abi: ResolverABI, + functionName: args.functionName, + args: args.data as never, + }); + await waitForTransactionReceipt(client, hash); + console.log(`[seed] ${args.log} tx=${hash}`); +} diff --git a/packages/ens-test-kit/src/seeder/fixtures/reverse-name.ts b/packages/ens-test-kit/src/seeder/fixtures/reverse-name.ts new file mode 100644 index 0000000000..8cc8d09a45 --- /dev/null +++ b/packages/ens-test-kit/src/seeder/fixtures/reverse-name.ts @@ -0,0 +1,45 @@ +import { L2ReverseRegistrarABI } from "@ensnode/datasources"; +import { contracts } from "@ensnode/datasources/devnet"; + +import type { ChainId, Hex, NormalizedName } from "../../types"; +import { waitForTransactionReceipt } from "../tx-receipts"; +import type { SeederContext } from "../types"; +import { buildFixture, type FixtureArgs, type FixtureMeta } from "./base"; +import { getSenderClient } from "./sender-client"; + +type ReverseNameFields = { + address: Hex; + chainId: ChainId; + name: NormalizedName; +}; + +export type ReverseNameFixture = FixtureMeta<"reverseName"> & ReverseNameFields; + +export function reverseName( + args: FixtureArgs<"reverseName", ReverseNameFields>, +): ReverseNameFixture { + return buildFixture("reverseName", args); +} + +export async function applyReverseNameFixture( + fixture: ReverseNameFixture, + ctx: SeederContext, +): Promise { + const senderClient = getSenderClient(ctx, fixture.sender); + const senderAddress = senderClient.account.address.toLowerCase(); + if (fixture.address.toLowerCase() !== senderAddress) { + throw new Error( + `Reverse-name fixture "${fixture.id}" targets ${fixture.address}, but selected sender is ${senderClient.account.address}.`, + ); + } + + const hash = await senderClient.writeContract({ + address: contracts.ethReverseRegistrar, + abi: L2ReverseRegistrarABI, + functionName: "setName", + args: [fixture.name], + }); + + await waitForTransactionReceipt(senderClient, hash); + console.log(`[seed] reverse-name ${fixture.name} tx=${hash}`); +} diff --git a/packages/ens-test-kit/src/seeder/fixtures/sender-client.ts b/packages/ens-test-kit/src/seeder/fixtures/sender-client.ts new file mode 100644 index 0000000000..13ba10d387 --- /dev/null +++ b/packages/ens-test-kit/src/seeder/fixtures/sender-client.ts @@ -0,0 +1,8 @@ +import type { SeederContext, SeederSender } from "../types"; + +export function getSenderClient( + ctx: SeederContext, + sender: SeederSender | undefined, +): SeederContext["clients"][SeederSender] { + return ctx.clients[sender ?? "owner"]; +} diff --git a/packages/ens-test-kit/src/seeder/fixtures/text-record.ts b/packages/ens-test-kit/src/seeder/fixtures/text-record.ts index 7501d22b82..5d47d03076 100644 --- a/packages/ens-test-kit/src/seeder/fixtures/text-record.ts +++ b/packages/ens-test-kit/src/seeder/fixtures/text-record.ts @@ -1,10 +1,35 @@ -import type { Hex, NormalizedName } from "../../types"; +import type { SeederContext } from "../types"; +import { + buildFixture, + type FixtureArgs, + type FixtureMeta, + type ResolverRecordFields, +} from "./base"; +import { assertExpectedResolver, getResolverAndNode, writeResolverTx } from "./resolver-utils"; +import { getSenderClient } from "./sender-client"; -export interface TextRecordFixture { - kind: "text-record"; - id: string; - name: NormalizedName; - resolverAddress?: Hex; +type TextRecordFields = ResolverRecordFields & { key: string; value: string; +}; + +export type TextRecordFixture = FixtureMeta<"textRecord"> & TextRecordFields; + +export function textRecord(args: FixtureArgs<"textRecord", TextRecordFields>): TextRecordFixture { + return buildFixture("textRecord", args); +} + +export async function applyTextRecordFixture( + fixture: TextRecordFixture, + ctx: SeederContext, +): Promise { + const senderClient = getSenderClient(ctx, fixture.sender); + const { node, resolver } = getResolverAndNode(fixture.name, fixture.resolverAddress); + await assertExpectedResolver(senderClient, fixture.name, resolver); + await writeResolverTx(senderClient, { + resolver, + functionName: "setText", + data: [node, fixture.key, fixture.value], + log: `text-record ${fixture.name} key=${fixture.key}`, + }); } diff --git a/packages/ens-test-kit/src/seeder/index.ts b/packages/ens-test-kit/src/seeder/index.ts index b4fa7779ce..52c10e5d6e 100644 --- a/packages/ens-test-kit/src/seeder/index.ts +++ b/packages/ens-test-kit/src/seeder/index.ts @@ -1,8 +1,23 @@ -export type { AbiFixture } from "./fixtures/abi"; -export type { ContenthashFixture } from "./fixtures/contenthash"; -export type { InterfaceImplementerFixture } from "./fixtures/interface-implementer"; -export type { MulticoinAddressFixture } from "./fixtures/multicoin-address"; -export type { PrimaryNameFixture } from "./fixtures/primary-name"; -export type { PubkeyFixture } from "./fixtures/pubkey"; +import { canonicalFixtures } from "./fixtures/common"; +export { canonicalFixtures }; + +export type { AbiRecordFixture } from "./fixtures/abi"; +export { abiRecord } from "./fixtures/abi"; +export type { ContenthashRecordFixture } from "./fixtures/contenthash"; +export { contenthashRecord } from "./fixtures/contenthash"; +export type { InterfaceRecordFixture } from "./fixtures/interface-record"; +export { interfaceRecord } from "./fixtures/interface-record"; +export type { MulticoinAddressRecordFixture } from "./fixtures/multicoin-address"; +export { multicoinAddressRecord } from "./fixtures/multicoin-address"; +export type { PubkeyRecordFixture } from "./fixtures/pubkey"; +export { pubkeyRecord } from "./fixtures/pubkey"; +export type { ReverseNameFixture } from "./fixtures/reverse-name"; +export { reverseName } from "./fixtures/reverse-name"; export type { TextRecordFixture } from "./fixtures/text-record"; -export type { Fixture, FixtureBase, FixtureKind, SeederContext } from "./types"; +export { textRecord } from "./fixtures/text-record"; +export { createSeederContext, dedupeFixtures, getFixtureSet, seedFixtures } from "./runtime"; +export { + seedReceiptWaitOptions, + waitForTransactionReceipt, +} from "./tx-receipts"; +export type { DevnetWalletClient, Fixture, FixtureBase, FixtureKind, SeederContext } from "./types"; diff --git a/packages/ens-test-kit/src/seeder/runtime.ts b/packages/ens-test-kit/src/seeder/runtime.ts new file mode 100644 index 0000000000..56346706de --- /dev/null +++ b/packages/ens-test-kit/src/seeder/runtime.ts @@ -0,0 +1,130 @@ +import { + type Account, + type Chain, + createWalletClient, + http, + type PublicActions, + publicActions, + type Transport, + type WalletClient, +} from "viem"; + +import { ensTestEnvChain } from "@ensnode/datasources"; +import { accounts } from "@ensnode/datasources/devnet"; + +import { applyAbiRecordFixture } from "./fixtures/abi"; +import { canonicalFixtures } from "./fixtures/common"; +import { applyContenthashRecordFixture } from "./fixtures/contenthash"; +import { applyInterfaceRecordFixture } from "./fixtures/interface-record"; +import { applyMulticoinAddressRecordFixture } from "./fixtures/multicoin-address"; +import { applyPubkeyRecordFixture } from "./fixtures/pubkey"; +import { applyReverseNameFixture } from "./fixtures/reverse-name"; +import { applyTextRecordFixture } from "./fixtures/text-record"; +import type { Fixture, SeederContext } from "./types"; + +function createDevnetWalletClient(transport: Transport, account: Account) { + return createWalletClient({ + chain: ensTestEnvChain, + transport, + account, + }).extend(publicActions); +} + +type DevnetWalletClient = WalletClient & PublicActions; + +function createDevnetWalletClients(rpcUrl: string): SeederContext["clients"] { + const transport = http(rpcUrl); + const makeClient = (account: Account): DevnetWalletClient => + createDevnetWalletClient(transport, account); + + return { + deployer: makeClient(accounts.deployer), + owner: makeClient(accounts.owner), + user: makeClient(accounts.user), + user2: makeClient(accounts.user2), + }; +} + +export function createSeederContext(rpcUrl: string): SeederContext { + return { + rpcUrl, + clients: createDevnetWalletClients(rpcUrl), + }; +} + +function stableStringify(value: unknown): string { + if (Array.isArray(value)) { + return `[${value.map((item) => stableStringify(item)).join(",")}]`; + } + if (value !== null && typeof value === "object") { + const entries = Object.entries(value as Record).sort(([a], [b]) => + a.localeCompare(b), + ); + return `{${entries + .map(([key, innerValue]) => `${JSON.stringify(key)}:${stableStringify(innerValue)}`) + .join(",")}}`; + } + return JSON.stringify(value); +} + +export function dedupeFixtures(fixtures: Fixture[]): Fixture[] { + const byId = new Map(); + + for (const fixture of fixtures) { + const existing = byId.get(fixture.id); + if (!existing) { + byId.set(fixture.id, fixture); + continue; + } + + const incomingComparable = stableStringify(fixture); + const existingComparable = stableStringify(existing); + + if (incomingComparable !== existingComparable) { + throw new Error( + [ + `Conflicting fixtures with id "${fixture.id}".`, + `existing=${JSON.stringify(existing)}`, + `incoming=${JSON.stringify(fixture)}`, + ].join(" "), + ); + } + } + + return [...byId.values()]; +} + +const HANDLERS: { + [K in Fixture["kind"]]: ( + fixture: Extract, + ctx: SeederContext, + ) => Promise; +} = { + reverseName: applyReverseNameFixture, + textRecord: applyTextRecordFixture, + multicoinAddressRecord: applyMulticoinAddressRecordFixture, + contenthashRecord: applyContenthashRecordFixture, + pubkeyRecord: applyPubkeyRecordFixture, + abiRecord: applyAbiRecordFixture, + interfaceRecord: applyInterfaceRecordFixture, +}; + +export async function seedFixtures(rpcUrl: string, fixtures: Fixture[]): Promise { + const context = createSeederContext(rpcUrl); + const deduped = dedupeFixtures(fixtures); + + for (const fixture of deduped) { + const handler = HANDLERS[fixture.kind] as ( + fixture: Fixture, + ctx: SeederContext, + ) => Promise; + await handler(fixture, context); + } + + return deduped; +} + +export function getFixtureSet(name: string): Fixture[] { + if (name === "canonical") return canonicalFixtures; + throw new Error(`Unknown fixture set "${name}". Supported sets: canonical.`); +} diff --git a/packages/ens-test-kit/src/seeder/tx-receipts.ts b/packages/ens-test-kit/src/seeder/tx-receipts.ts new file mode 100644 index 0000000000..af83d78be6 --- /dev/null +++ b/packages/ens-test-kit/src/seeder/tx-receipts.ts @@ -0,0 +1,19 @@ +import type { Hex } from "viem"; + +import type { DevnetWalletClient } from "./types"; + +export const seedReceiptWaitOptions = { + pollingInterval: 50, + confirmations: 1, + timeout: 15_000, +} as const; + +export async function waitForTransactionReceipt( + client: DevnetWalletClient, + hash: Hex, +): Promise { + await client.waitForTransactionReceipt({ + hash, + ...seedReceiptWaitOptions, + }); +} diff --git a/packages/ens-test-kit/src/seeder/types.ts b/packages/ens-test-kit/src/seeder/types.ts index ea4111728b..bf0dc8c2d5 100644 --- a/packages/ens-test-kit/src/seeder/types.ts +++ b/packages/ens-test-kit/src/seeder/types.ts @@ -1,29 +1,37 @@ -import type { AbiFixture } from "./fixtures/abi"; -import type { ContenthashFixture } from "./fixtures/contenthash"; -import type { InterfaceImplementerFixture } from "./fixtures/interface-implementer"; -import type { MulticoinAddressFixture } from "./fixtures/multicoin-address"; -import type { PrimaryNameFixture } from "./fixtures/primary-name"; -import type { PubkeyFixture } from "./fixtures/pubkey"; +import type { Account, Chain, PublicActions, Transport, WalletClient } from "viem"; + +import type { AbiRecordFixture } from "./fixtures/abi"; +import type { ContenthashRecordFixture } from "./fixtures/contenthash"; +import type { InterfaceRecordFixture } from "./fixtures/interface-record"; +import type { MulticoinAddressRecordFixture } from "./fixtures/multicoin-address"; +import type { PubkeyRecordFixture } from "./fixtures/pubkey"; +import type { ReverseNameFixture } from "./fixtures/reverse-name"; import type { TextRecordFixture } from "./fixtures/text-record"; export type Fixture = - | PrimaryNameFixture + | ReverseNameFixture | TextRecordFixture - | MulticoinAddressFixture - | ContenthashFixture - | PubkeyFixture - | AbiFixture - | InterfaceImplementerFixture; + | MulticoinAddressRecordFixture + | ContenthashRecordFixture + | PubkeyRecordFixture + | AbiRecordFixture + | InterfaceRecordFixture; export type FixtureKind = Fixture["kind"]; -export type FixtureBase = Pick; +export type FixtureBase = Pick; + +export type DevnetWalletClient = WalletClient & PublicActions; + +export type SeederClients = { + deployer: DevnetWalletClient; + owner: DevnetWalletClient; + user: DevnetWalletClient; + user2: DevnetWalletClient; +}; + +export type SeederSender = keyof SeederClients; export interface SeederContext { rpcUrl: string; - clients: { - deployer: unknown; - owner: unknown; - user: unknown; - user2: unknown; - }; + clients: SeederClients; } diff --git a/packages/ens-test-kit/tsup.config.ts b/packages/ens-test-kit/tsup.config.ts index d58f21a3fd..ab0aff1939 100644 --- a/packages/ens-test-kit/tsup.config.ts +++ b/packages/ens-test-kit/tsup.config.ts @@ -9,6 +9,7 @@ export default defineConfig({ "seeder/index": "src/seeder/index.ts", "vitest/index": "src/vitest/index.ts", "cli/index": "src/cli/index.ts", + "cli/seed": "src/cli/seed.ts", }, platform: "neutral", format: ["esm"], diff --git a/packages/integration-test-env/src/orchestrator.ts b/packages/integration-test-env/src/orchestrator.ts index 3f10e192dd..b050884291 100644 --- a/packages/integration-test-env/src/orchestrator.ts +++ b/packages/integration-test-env/src/orchestrator.ts @@ -45,8 +45,6 @@ import { OmnichainIndexingStatusIds, } from "@ensnode/ensnode-sdk"; -import { seedDevnet } from "./seed/index"; - const MONOREPO_ROOT = resolve(import.meta.dirname, "../../.."); const DOCKER_DIR = resolve(MONOREPO_ROOT, "docker"); const ENSRAINBOW_DIR = resolve(MONOREPO_ROOT, "apps/ensrainbow"); @@ -279,12 +277,7 @@ async function main() { log(`Devnet is ready (RPC URL: ${devnetRpcUrl})`); - // Phase 2: Seed devnet with test data (before indexing starts) - log("Seeding devnet..."); - await seedDevnet(devnetRpcUrl); - log("Devnet seeded"); - - // Phase 3: Download ENSRainbow database and start from source + // Phase 2: Download ENSRainbow database and start from source const DB_SCHEMA_VERSION = "3"; const LABEL_SET_ID = "ens-test-env"; const LABEL_SET_VERSION = "0"; @@ -305,7 +298,7 @@ async function main() { // /ready returns 200 only after the DB has been downloaded, extracted, and attached. await waitForHealth(`http://localhost:${ENSRAINBOW_PORT}/ready`, 30_000, "ENSRainbow"); - // Phase 4: Start ENSIndexer + // Phase 3: Start ENSIndexer log("Starting ENSIndexer..."); spawnService( "pnpm", @@ -324,10 +317,10 @@ async function main() { ); await waitForHealth(`http://localhost:${ENSINDEXER_PORT}/health`, 60_000, "ENSIndexer"); - // Phase 5: Wait for indexing to complete + // Phase 4: Wait for indexing to complete await pollIndexingStatus(ENSDB_URL, ENSINDEXER_SCHEMA_NAME, 30_000); - // Phase 6: Start ENSApi + // Phase 5: Start ENSApi log("Starting ENSApi..."); spawnService( "pnpm", @@ -341,7 +334,7 @@ async function main() { ); await waitForHealth(`http://localhost:${ENSAPI_PORT}/health`, 10_000, "ENSApi"); - // Phase 7: Run integration tests + // Phase 6: Run integration tests log("Running integration tests..."); execaSync("pnpm", ["test:integration", "--", "--bail", "1"], { cwd: MONOREPO_ROOT, diff --git a/packages/integration-test-env/src/seed/index.ts b/packages/integration-test-env/src/seed/index.ts deleted file mode 100644 index b8d916bca1..0000000000 --- a/packages/integration-test-env/src/seed/index.ts +++ /dev/null @@ -1,68 +0,0 @@ -import type { Hex } from "viem"; -import { - type Account, - type Chain, - createWalletClient, - http, - type PublicActions, - publicActions, - type Transport, - type WalletClient, -} from "viem"; - -import { ensTestEnvChain } from "@ensnode/datasources"; -import { accounts } from "@ensnode/datasources/devnet"; - -import { seedPrimaryNameRecords } from "./primary-names"; -import { seedResolverRecords } from "./resolver-records"; - -function createDevnetWalletClient(transport: Transport, account: Account) { - return createWalletClient({ - chain: ensTestEnvChain, - transport, - account, - }).extend(publicActions); -} - -export type DevnetWalletClient = WalletClient & PublicActions; - -export type DevnetWalletClients = { - deployer: DevnetWalletClient; // index 0 - owner: DevnetWalletClient; // index 1 - user: DevnetWalletClient; // index 2 - user2: DevnetWalletClient; // index 3 -}; - -function createDevnetWalletClients(rpcUrl: string): DevnetWalletClients { - const transport = http(rpcUrl); - const makeClient = (account: Account): DevnetWalletClient => - createDevnetWalletClient(transport, account); - return { - deployer: makeClient(accounts.deployer), - owner: makeClient(accounts.owner), - user: makeClient(accounts.user), - user2: makeClient(accounts.user2), - }; -} - -export const seedReceiptWaitOptions = { - pollingInterval: 50, - confirmations: 1, - timeout: 15_000, -} as const; - -export async function waitForTransactionReceipt( - client: DevnetWalletClient, - hash: Hex, -): Promise { - await client.waitForTransactionReceipt({ - hash, - ...seedReceiptWaitOptions, - }); -} - -export async function seedDevnet(rpcUrl: string): Promise { - const clients = createDevnetWalletClients(rpcUrl); - await seedPrimaryNameRecords(clients); - await seedResolverRecords(clients); -} diff --git a/packages/integration-test-env/src/seed/primary-names.ts b/packages/integration-test-env/src/seed/primary-names.ts deleted file mode 100644 index 2b237d3dbc..0000000000 --- a/packages/integration-test-env/src/seed/primary-names.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { L2ReverseRegistrarABI } from "@ensnode/datasources"; -import { contracts } from "@ensnode/datasources/devnet"; - -import type { DevnetWalletClient, DevnetWalletClients } from "./index"; -import { waitForTransactionReceipt } from "./index"; - -export async function seedPrimaryNameRecords(clients: DevnetWalletClients): Promise { - await setPrimaryNameRecord(clients.owner, "test.eth"); -} - -async function setPrimaryNameRecord(walletClient: DevnetWalletClient, name: string): Promise { - const hash = await walletClient.writeContract({ - address: contracts.ethReverseRegistrar, - abi: L2ReverseRegistrarABI, - functionName: "setName", - args: [name], - }); - await waitForTransactionReceipt(walletClient, hash); - console.log(`[seed] setPrimaryNameRecord("${name}") tx: ${hash}`); -} diff --git a/packages/integration-test-env/src/seed/resolver-records.ts b/packages/integration-test-env/src/seed/resolver-records.ts deleted file mode 100644 index 499cd12034..0000000000 --- a/packages/integration-test-env/src/seed/resolver-records.ts +++ /dev/null @@ -1,163 +0,0 @@ -import { type Address, type Hex, namehash, toHex } from "viem"; -import { packetToBytes } from "viem/ens"; - -import { ResolverABI, UniversalResolverABI } from "@ensnode/datasources"; -import { addresses, contracts, fixtures } from "@ensnode/datasources/devnet"; - -import type { DevnetWalletClient, DevnetWalletClients } from "./index"; -import { waitForTransactionReceipt } from "./index"; - -export async function seedResolverRecords(clients: DevnetWalletClients): Promise { - await seedResolverRecordsForName(clients, "test.eth", contracts.permissionedResolver); -} - -async function seedResolverRecordsForName( - clients: DevnetWalletClients, - name: string, - resolver: Address, -): Promise { - const node = namehash(name); - const actualResolver = await findResolver(clients.owner, name); - if (actualResolver.toLowerCase() !== resolver.toLowerCase()) { - throw new Error( - `${name} resolver mismatch: active=${actualResolver}, expected=${resolver}. Either resolver has been changed or something else is wrong.`, - ); - } - - // Text records - await setTextRecord(clients.owner, resolver, node, "avatar", "https://example.com/avatar.png"); - await setTextRecord(clients.owner, resolver, node, "com.twitter", "ensdomains"); - await setTextRecord(clients.owner, resolver, node, "com.github", "ensdomains"); - await setTextRecord(clients.owner, resolver, node, "url", "https://ens.domains"); - await setTextRecord(clients.owner, resolver, node, "email", "test@ens.domains"); - await setTextRecord(clients.owner, resolver, node, "description", "test.eth"); - - // Multi-coin addresses - // Coin 0 = Bitcoin - await setMulticoinAddress(clients.owner, resolver, node, 0n, fixtures.bitcoinAddress); - // Coin 2 = Litecoin - await setMulticoinAddress(clients.owner, resolver, node, 2n, fixtures.litecoinAddress); - - // Scalar resolver records - await setContenthash(clients.owner, resolver, node, fixtures.contenthash); - await setPubkey(clients.owner, resolver, node, fixtures.publicKeyX, fixtures.publicKeyY); - await setAbi(clients.owner, resolver, node, 1n, fixtures.abiBytes); - await setInterfaceImplementer( - clients.owner, - resolver, - node, - fixtures.fourBytesInterface, - addresses.one, - ); -} - -async function findResolver(client: DevnetWalletClient, name: string): Promise
{ - const [resolver] = await client.readContract({ - address: contracts.universalResolverV2, - abi: UniversalResolverABI, - functionName: "findResolver", - args: [toHex(packetToBytes(name))], - }); - return resolver; -} - -async function setTextRecord( - walletClient: DevnetWalletClient, - resolver: Address, - node: Hex, - key: string, - value: string, -): Promise { - const hash = await walletClient.writeContract({ - address: resolver, - abi: ResolverABI, - functionName: "setText", - args: [node, key, value], - }); - await waitForTransactionReceipt(walletClient, hash); - console.log(`[seed] setText("${key}", "${value}") tx: ${hash}`); -} - -async function setMulticoinAddress( - walletClient: DevnetWalletClient, - resolver: Address, - node: Hex, - coinType: bigint, - addressBytes: Hex, -): Promise { - const hash = await walletClient.writeContract({ - address: resolver, - abi: ResolverABI, - functionName: "setAddr", - args: [node, coinType, addressBytes], - }); - await waitForTransactionReceipt(walletClient, hash); - console.log(`[seed] setAddr(coinType=${coinType}) tx: ${hash}`); -} - -async function setContenthash( - walletClient: DevnetWalletClient, - resolver: Address, - node: Hex, - hashValue: Hex, -): Promise { - const hash = await walletClient.writeContract({ - address: resolver, - abi: ResolverABI, - functionName: "setContenthash", - args: [node, hashValue], - }); - await waitForTransactionReceipt(walletClient, hash); - console.log(`[seed] setContenthash() tx: ${hash}`); -} - -async function setPubkey( - walletClient: DevnetWalletClient, - resolver: Address, - node: Hex, - x: Hex, - y: Hex, -): Promise { - const hash = await walletClient.writeContract({ - address: resolver, - abi: ResolverABI, - functionName: "setPubkey", - args: [node, x, y], - }); - await waitForTransactionReceipt(walletClient, hash); - console.log(`[seed] setPubkey() tx: ${hash}`); -} - -async function setAbi( - walletClient: DevnetWalletClient, - resolver: Address, - node: Hex, - contentType: bigint, - data: Hex, -): Promise { - const hash = await walletClient.writeContract({ - address: resolver, - abi: ResolverABI, - functionName: "setABI", - args: [node, contentType, data], - }); - await waitForTransactionReceipt(walletClient, hash); - console.log(`[seed] setABI(contentType=${contentType}) tx: ${hash}`); -} - -async function setInterfaceImplementer( - walletClient: DevnetWalletClient, - resolver: Address, - node: Hex, - interfaceId: Hex, - implementer: Address, -): Promise { - const hash = await walletClient.writeContract({ - address: resolver, - abi: ResolverABI, - functionName: "setInterface", - args: [node, interfaceId, implementer], - }); - await waitForTransactionReceipt(walletClient, hash); - console.log(`[seed] setInterface(interfaceId=${interfaceId}) tx: ${hash}`); -} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 1a4b7caafb..03f33e3a30 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -901,6 +901,13 @@ importers: version: 4.0.5(@types/debug@4.1.12)(@types/node@24.10.9)(jiti@2.6.1)(jsdom@27.0.1(postcss@8.5.12))(lightningcss@1.30.2)(tsx@4.21.0)(yaml@2.8.3) packages/ens-test-kit: + dependencies: + '@ensnode/datasources': + specifier: workspace:* + version: link:../datasources + viem: + specifier: 'catalog:' + version: 2.38.5(typescript@5.9.3)(zod@4.3.6) devDependencies: '@ensnode/shared-configs': specifier: workspace:* @@ -914,9 +921,6 @@ importers: typescript: specifier: 'catalog:' version: 5.9.3 - viem: - specifier: 'catalog:' - version: 2.38.5(typescript@5.9.3)(zod@4.3.6) vitest: specifier: 'catalog:' version: 4.0.5(@types/debug@4.1.12)(@types/node@24.10.9)(jiti@2.6.1)(jsdom@27.0.1(postcss@8.5.6))(lightningcss@1.30.2)(tsx@4.21.0)(yaml@2.8.3) @@ -1172,9 +1176,6 @@ importers: tsx: specifier: ^4.7.1 version: 4.20.6 - viem: - specifier: 'catalog:' - version: 2.38.5(typescript@5.9.3)(zod@4.3.6) packages/namehash-ui: dependencies: From cc089a12cadda15d219783367059c63c1ff25208 Mon Sep 17 00:00:00 2001 From: sevenzing Date: Fri, 1 May 2026 23:02:31 +0400 Subject: [PATCH 17/18] add expectations DSL, vitest runner, and first testcases --- .../resolve-primary-name.integration.test.ts | 28 --- .../resolve-primary-names.integration.test.ts | 22 -- .../resolve-records.integration.test.ts | 221 ------------------ packages/ens-test-kit/bin/ens-test-kit.mjs | 0 .../ens-test-kit/src/cases/expectation.ts | 24 ++ packages/ens-test-kit/src/cases/index.ts | 4 + .../src/cases/resolution/forward.ts | 175 ++++++++++++++ .../src/cases/resolution/reverse.ts | 43 ++++ packages/ens-test-kit/src/vitest/index.ts | 4 +- packages/ens-test-kit/src/vitest/run-suite.ts | 31 +++ packages/integration-test-env/package.json | 1 + .../src/adapters/rest-adapter.ts | 76 ++++++ .../tests/resolution-rest.integration.test.ts | 8 + .../vitest.integration.config.ts | 7 + pnpm-lock.yaml | 3 + 15 files changed, 373 insertions(+), 274 deletions(-) mode change 100644 => 100755 packages/ens-test-kit/bin/ens-test-kit.mjs create mode 100644 packages/ens-test-kit/src/cases/expectation.ts create mode 100644 packages/ens-test-kit/src/cases/resolution/forward.ts create mode 100644 packages/ens-test-kit/src/cases/resolution/reverse.ts create mode 100644 packages/ens-test-kit/src/vitest/run-suite.ts create mode 100644 packages/integration-test-env/src/adapters/rest-adapter.ts create mode 100644 packages/integration-test-env/src/tests/resolution-rest.integration.test.ts create mode 100644 packages/integration-test-env/vitest.integration.config.ts diff --git a/apps/ensapi/src/handlers/api/resolution/resolve-primary-name.integration.test.ts b/apps/ensapi/src/handlers/api/resolution/resolve-primary-name.integration.test.ts index 7f928e1609..c85b6fd3b5 100644 --- a/apps/ensapi/src/handlers/api/resolution/resolve-primary-name.integration.test.ts +++ b/apps/ensapi/src/handlers/api/resolution/resolve-primary-name.integration.test.ts @@ -12,34 +12,6 @@ const BASE_URL = process.env.ENSNODE_URL!; describe("GET /api/resolve/primary-name/:address/:chainId", () => { it.each([ - { - description: "resolves primary name for owner address on chain 1", - address: accounts.owner.address, - chainId: "1", - query: "", - expectedStatus: 200, - expectedBody: { - name: "test.eth", - accelerationRequested: false, - accelerationAttempted: false, - }, - }, - { - description: "returns null for user without a primary name", - address: accounts.user.address, - chainId: "1", - query: "", - expectedStatus: 200, - expectedBody: { name: null, accelerationRequested: false, accelerationAttempted: false }, - }, - { - description: "owner address with accelerate=true returns accelerationRequested: true", - address: accounts.owner.address, - chainId: "1", - query: "accelerate=true", - expectedStatus: 200, - expectedBody: { accelerationRequested: true, accelerationAttempted: false }, - }, { description: "returns 400 for invalid (non-hex) address", address: "notanaddress", diff --git a/apps/ensapi/src/handlers/api/resolution/resolve-primary-names.integration.test.ts b/apps/ensapi/src/handlers/api/resolution/resolve-primary-names.integration.test.ts index 6625025f4f..10521ffea4 100644 --- a/apps/ensapi/src/handlers/api/resolution/resolve-primary-names.integration.test.ts +++ b/apps/ensapi/src/handlers/api/resolution/resolve-primary-names.integration.test.ts @@ -12,28 +12,6 @@ const BASE_URL = process.env.ENSNODE_URL!; describe("GET /api/resolve/primary-names/:address", () => { it.each([ - { - description: "resolves primary names for owner address on chain 1", - address: accounts.owner.address, - query: "chainIds=1", - expectedStatus: 200, - expectedBody: { - names: { "1": "test.eth" }, - accelerationRequested: false, - accelerationAttempted: false, - }, - }, - { - description: "resolves all primary names", - address: accounts.owner.address, - query: "", - expectedStatus: 200, - expectedBody: { - names: { "1": "test.eth" }, - accelerationRequested: false, - accelerationAttempted: false, - }, - }, { description: "returns 400 for invalid (non-hex) address", address: "notanaddress", diff --git a/apps/ensapi/src/handlers/api/resolution/resolve-records.integration.test.ts b/apps/ensapi/src/handlers/api/resolution/resolve-records.integration.test.ts index a908cfe091..5ce46b7308 100644 --- a/apps/ensapi/src/handlers/api/resolution/resolve-records.integration.test.ts +++ b/apps/ensapi/src/handlers/api/resolution/resolve-records.integration.test.ts @@ -6,231 +6,10 @@ import { describe, expect, it } from "vitest"; -import { accounts, addresses, fixtures } from "@ensnode/datasources/devnet"; - const BASE_URL = process.env.ENSNODE_URL!; describe("GET /api/resolve/records/:name", () => { it.each([ - { - description: "resolves ETH address (coin 60) for test.eth", - name: "test.eth", - query: "addresses=60", - expectedStatus: 200, - expectedBody: { - records: { addresses: { 60: accounts.owner.address } }, - accelerationRequested: false, - accelerationAttempted: false, - }, - }, - { - description: - "resolves ETH address for newowner.eth (coin 60 stays as original registrant after token transfer)", - name: "newowner.eth", - query: "addresses=60", - expectedStatus: 200, - expectedBody: { - records: { addresses: { 60: accounts.owner.address } }, - accelerationRequested: false, - accelerationAttempted: false, - }, - }, - { - description: "resolves description text record for example.eth", - name: "example.eth", - query: "texts=description", - expectedStatus: 200, - expectedBody: { - records: { texts: { description: "example.eth" } }, - accelerationRequested: false, - accelerationAttempted: false, - }, - }, - { - description: - "resolves description text record for alias.eth (resolves via alias to test.eth)", - name: "alias.eth", - query: "texts=description", - expectedStatus: 200, - expectedBody: { - records: { texts: { description: "test.eth" } }, - accelerationRequested: false, - accelerationAttempted: false, - }, - }, - { - description: "resolves both address and text records for example.eth", - name: "example.eth", - query: "addresses=60&texts=description", - expectedStatus: 200, - expectedBody: { - records: { - addresses: { 60: accounts.owner.address }, - texts: { description: "example.eth" }, - }, - accelerationRequested: false, - accelerationAttempted: false, - }, - }, - { - description: "returns null address for reserved.eth (no resolver)", - name: "reserved.eth", - query: "addresses=60", - expectedStatus: 200, - expectedBody: { - records: { addresses: { 60: null } }, - accelerationRequested: false, - accelerationAttempted: false, - }, - }, - { - description: - "returns old coin 60 record for sub.unregistered.eth (token burned but resolver records persist)", - name: "sub.unregistered.eth", - query: "addresses=60", - expectedStatus: 200, - expectedBody: { - records: { addresses: { 60: accounts.owner.address } }, - accelerationRequested: false, - accelerationAttempted: false, - }, - }, - { - description: "returns null address for nonexistent name", - name: "thisnamedoesnotexist.eth", - query: "addresses=60", - expectedStatus: 200, - expectedBody: { - records: { addresses: { 60: null } }, - accelerationRequested: false, - accelerationAttempted: false, - }, - }, - { - description: "resolves ETH address for linked.parent.eth (alias to sub1.sub2.parent.eth)", - name: "linked.parent.eth", - query: "addresses=60", - expectedStatus: 200, - expectedBody: { - records: { addresses: { 60: accounts.owner.address } }, - accelerationRequested: false, - accelerationAttempted: false, - }, - }, - { - description: - "resolves ETH address for wallet.linked.parent.eth (alias to wallet.sub1.sub2.parent.eth)", - name: "wallet.linked.parent.eth", - query: "addresses=60", - expectedStatus: 200, - expectedBody: { - records: { addresses: { 60: accounts.owner.address } }, - accelerationRequested: false, - accelerationAttempted: false, - }, - }, - // -- Text records (seeded in devnet) -- - { - description: "resolves avatar text record for test.eth", - name: "test.eth", - query: "texts=avatar", - expectedStatus: 200, - expectedBody: { - records: { texts: { avatar: "https://example.com/avatar.png" } }, - accelerationRequested: false, - accelerationAttempted: false, - }, - }, - { - description: "returns null for unset text record", - name: "test.eth", - query: "texts=nonexistent.key", - expectedStatus: 200, - expectedBody: { - records: { texts: { "nonexistent.key": null } }, - accelerationRequested: false, - accelerationAttempted: false, - }, - }, - // -- Multi-coin addresses (seeded in devnet) -- - { - description: "resolves multiple coin types at once for test.eth", - name: "test.eth", - query: "addresses=60,0,2,777777", - expectedStatus: 200, - expectedBody: { - records: { - addresses: { - 60: accounts.owner.address, - 0: fixtures.bitcoinAddress, - 2: fixtures.litecoinAddress, - 777777: null, - }, - }, - accelerationRequested: false, - accelerationAttempted: false, - }, - }, - // -- Combined records -- - { - description: "resolves every supported record type for test.eth", - name: "test.eth", - query: [ - "name=true", - "addresses=60,0,2", - "texts=avatar,description,url,email,com.twitter,com.github", - "contenthash=true", - "pubkey=true", - "version=true", - "abi=1", - `interfaces=${fixtures.fourBytesInterface}`, - ].join("&"), - expectedStatus: 200, - expectedBody: { - records: { - addresses: { - 60: accounts.owner.address, - 0: fixtures.bitcoinAddress, - 2: fixtures.litecoinAddress, - }, - texts: { - avatar: "https://example.com/avatar.png", - description: "test.eth", - url: "https://ens.domains", - email: "test@ens.domains", - "com.twitter": "ensdomains", - "com.github": "ensdomains", - }, - contenthash: fixtures.contenthash, - pubkey: { - x: fixtures.publicKeyX, - y: fixtures.publicKeyY, - }, - version: expect.any(String), - abi: { - contentType: "1", - data: fixtures.abiBytes, - }, - interfaces: { - [fixtures.fourBytesInterface]: addresses.one, - }, - }, - accelerationRequested: false, - accelerationAttempted: false, - }, - }, - // -- Acceleration -- - { - description: "test.eth with accelerate=true returns accelerationRequested: true", - name: "test.eth", - query: "addresses=60&accelerate=true", - expectedStatus: 200, - expectedBody: { - records: { addresses: { 60: accounts.owner.address } }, - accelerationRequested: true, - accelerationAttempted: false, - }, - }, { description: "returns 400 when selection is empty (no addresses, texts, or name)", name: "test.eth", diff --git a/packages/ens-test-kit/bin/ens-test-kit.mjs b/packages/ens-test-kit/bin/ens-test-kit.mjs old mode 100644 new mode 100755 diff --git a/packages/ens-test-kit/src/cases/expectation.ts b/packages/ens-test-kit/src/cases/expectation.ts new file mode 100644 index 0000000000..b201ff0d20 --- /dev/null +++ b/packages/ens-test-kit/src/cases/expectation.ts @@ -0,0 +1,24 @@ +export const EXPECTATION = Symbol.for("ens-test-kit.expectation"); + +export type Expectation = + | { [EXPECTATION]: "partial"; value: unknown } + | { [EXPECTATION]: "equals"; value: unknown } + | { [EXPECTATION]: "arrayContains"; items: unknown[] }; + +export const expectation = { + partial(value: unknown): Expectation { + return { [EXPECTATION]: "partial", value }; + }, + equals(value: unknown): Expectation { + return { [EXPECTATION]: "equals", value }; + }, + arrayContains(...items: unknown[]): Expectation { + return { [EXPECTATION]: "arrayContains", items }; + }, +}; + +export function isExpectation(value: unknown): value is Expectation { + if (typeof value !== "object" || value === null) return false; + const marker = (value as Record)[EXPECTATION]; + return marker === "partial" || marker === "equals" || marker === "arrayContains"; +} diff --git a/packages/ens-test-kit/src/cases/index.ts b/packages/ens-test-kit/src/cases/index.ts index f099478e18..60f3b26c8a 100644 --- a/packages/ens-test-kit/src/cases/index.ts +++ b/packages/ens-test-kit/src/cases/index.ts @@ -1 +1,5 @@ +export type { Expectation } from "./expectation"; +export { EXPECTATION, expectation, isExpectation } from "./expectation"; +export { forwardResolutionCases } from "./resolution/forward"; +export { reverseResolutionCases } from "./resolution/reverse"; export type { TestCase } from "./types"; diff --git a/packages/ens-test-kit/src/cases/resolution/forward.ts b/packages/ens-test-kit/src/cases/resolution/forward.ts new file mode 100644 index 0000000000..51ace836d9 --- /dev/null +++ b/packages/ens-test-kit/src/cases/resolution/forward.ts @@ -0,0 +1,175 @@ +import { accounts, addresses, fixtures } from "@ensnode/datasources/devnet"; + +import type { ResolutionsApi } from "../../interfaces/resolutions"; +import { + abiRecord, + contenthashRecord, + interfaceRecord, + multicoinAddressRecord, + pubkeyRecord, + textRecord, +} from "../../seeder"; +import type { TestCase } from "../types"; + +const textAvatar = textRecord({ + id: "resolution-forward-text-avatar", + name: "test.eth", + key: "avatar", + value: "https://example.com/avatar.png", +}); +const textTwitter = textRecord({ + id: "resolution-forward-text-com-twitter", + name: "test.eth", + key: "com.twitter", + value: "ensdomains", +}); +const textGithub = textRecord({ + id: "resolution-forward-text-com-github", + name: "test.eth", + key: "com.github", + value: "ensdomains", +}); +const textUrl = textRecord({ + id: "resolution-forward-text-url", + name: "test.eth", + key: "url", + value: "https://ens.domains", +}); +const textEmail = textRecord({ + id: "resolution-forward-text-email", + name: "test.eth", + key: "email", + value: "test@ens.domains", +}); +const textDescription = textRecord({ + id: "resolution-forward-text-description", + name: "test.eth", + key: "description", + value: "test.eth", +}); +const btcAddress = multicoinAddressRecord({ + id: "resolution-forward-address-btc", + name: "test.eth", + coinType: 0, + value: fixtures.bitcoinAddress, +}); +const ltcAddress = multicoinAddressRecord({ + id: "resolution-forward-address-ltc", + name: "test.eth", + coinType: 2, + value: fixtures.litecoinAddress, +}); +const testEthContenthash = contenthashRecord({ + id: "resolution-forward-contenthash", + name: "test.eth", + value: fixtures.contenthash, +}); +const testEthPubkey = pubkeyRecord({ + id: "resolution-forward-pubkey", + name: "test.eth", + x: fixtures.publicKeyX, + y: fixtures.publicKeyY, +}); +const testEthAbi = abiRecord({ + id: "resolution-forward-abi-content-type-1", + name: "test.eth", + contentType: 1, + value: fixtures.abiBytes, +}); +const testEthInterface = interfaceRecord({ + id: "resolution-forward-interface-record", + name: "test.eth", + interfaceId: fixtures.fourBytesInterface, + value: addresses.one, +}); + +export const forwardResolutionCases: TestCase[] = [ + { + id: "resolution.forward.text-avatar", + description: "resolves avatar text record for test.eth", + fixtures: [textAvatar], + call: (api) => api.resolveRecords("test.eth", { texts: [textAvatar.key] }), + expected: { texts: { [textAvatar.key]: textAvatar.value } }, + }, + { + id: "resolution.forward.text-unset", + description: "returns null for unset text record on test.eth", + fixtures: [], + call: (api) => api.resolveRecords("test.eth", { texts: ["nonexistent.key"] }), + expected: { texts: { "nonexistent.key": null } }, + }, + { + id: "resolution.forward.multicoin-selection", + description: "resolves selected multicoin addresses for test.eth", + fixtures: [btcAddress, ltcAddress], + call: (api) => api.resolveRecords("test.eth", { addresses: [0, 2, 777777] }), + expected: { + addresses: { + [btcAddress.coinType]: btcAddress.value, + [ltcAddress.coinType]: ltcAddress.value, + 777777: null, + }, + }, + }, + { + id: "resolution.forward.combined-supported-records", + description: "resolves all supported record types for test.eth", + fixtures: [ + textAvatar, + textTwitter, + textGithub, + textUrl, + textEmail, + textDescription, + btcAddress, + ltcAddress, + testEthContenthash, + testEthPubkey, + testEthAbi, + testEthInterface, + ], + call: (api) => + api.resolveRecords("test.eth", { + addresses: [60, btcAddress.coinType, ltcAddress.coinType], + texts: [ + textAvatar.key, + textDescription.key, + textUrl.key, + textEmail.key, + textTwitter.key, + textGithub.key, + ], + contenthash: true, + pubkey: true, + abi: true, + interfaceIds: [testEthInterface.interfaceId], + }), + expected: { + addresses: { + 60: accounts.owner.address, + [btcAddress.coinType]: btcAddress.value, + [ltcAddress.coinType]: ltcAddress.value, + }, + texts: { + [textAvatar.key]: textAvatar.value, + [textDescription.key]: textDescription.value, + [textUrl.key]: textUrl.value, + [textEmail.key]: textEmail.value, + [textTwitter.key]: textTwitter.value, + [textGithub.key]: textGithub.value, + }, + contenthash: testEthContenthash.value, + pubkey: { + x: testEthPubkey.x, + y: testEthPubkey.y, + }, + abi: { + contentType: String(testEthAbi.contentType), + data: testEthAbi.value, + }, + interfaces: { + [testEthInterface.interfaceId]: testEthInterface.value, + }, + }, + }, +]; diff --git a/packages/ens-test-kit/src/cases/resolution/reverse.ts b/packages/ens-test-kit/src/cases/resolution/reverse.ts new file mode 100644 index 0000000000..c272f8bd14 --- /dev/null +++ b/packages/ens-test-kit/src/cases/resolution/reverse.ts @@ -0,0 +1,43 @@ +import { accounts } from "@ensnode/datasources/devnet"; + +import type { ResolutionsApi } from "../../interfaces/resolutions"; +import { reverseName } from "../../seeder"; +import type { TestCase } from "../types"; + +const ownerReverseFixture = reverseName({ + id: "resolution-reverse-owner-test-eth", + address: accounts.owner.address, + chainId: 1, + name: "test.eth", +}); + +export const reverseResolutionCases: TestCase[] = [ + { + id: "resolution.reverse.single-owner-chain-1", + description: "resolves owner primary name on chain 1", + fixtures: [ownerReverseFixture], + call: (api) => api.resolvePrimaryName(accounts.owner.address, 1), + expected: ownerReverseFixture.name, + }, + { + id: "resolution.reverse.single-user-null", + description: "returns null for user without a primary name", + fixtures: [], + call: (api) => api.resolvePrimaryName(accounts.user.address, 1), + expected: null, + }, + { + id: "resolution.reverse.multi-owner-chain-1", + description: "resolves owner primary names map for explicit chain set", + fixtures: [ownerReverseFixture], + call: (api) => api.resolvePrimaryNames(accounts.owner.address, [1]), + expected: { 1: ownerReverseFixture.name }, + }, + { + id: "resolution.reverse.multi-owner-all", + description: "resolves owner primary names map for default chain selection", + fixtures: [ownerReverseFixture], + call: (api) => api.resolvePrimaryNames(accounts.owner.address), + expected: { 1: ownerReverseFixture.name }, + }, +]; diff --git a/packages/ens-test-kit/src/vitest/index.ts b/packages/ens-test-kit/src/vitest/index.ts index 348bcdf617..c147d1fa9b 100644 --- a/packages/ens-test-kit/src/vitest/index.ts +++ b/packages/ens-test-kit/src/vitest/index.ts @@ -1,3 +1 @@ -import type { TestCase } from "../cases/types"; - -export type RunSuite = (adapter: Api, cases: TestCase[]) => void; +export { runSuite } from "./run-suite"; diff --git a/packages/ens-test-kit/src/vitest/run-suite.ts b/packages/ens-test-kit/src/vitest/run-suite.ts new file mode 100644 index 0000000000..69b1df48df --- /dev/null +++ b/packages/ens-test-kit/src/vitest/run-suite.ts @@ -0,0 +1,31 @@ +import { describe, expect, it } from "vitest"; + +import { EXPECTATION, type Expectation, isExpectation } from "../cases/expectation"; +import type { TestCase } from "../cases/types"; + +export function runSuite(adapter: Api, cases: TestCase[]): void { + describe(`suite (${cases.length} cases)`, () => { + it.each(cases)("$id - $description", async (testCase) => { + const actual = await testCase.call(adapter); + if (isExpectation(testCase.expected)) { + assertExpectation(actual, testCase.expected); + } else { + expect(actual).toMatchObject(testCase.expected as object); + } + }); + }); +} + +function assertExpectation(actual: unknown, e: Expectation): void { + switch (e[EXPECTATION]) { + case "partial": + expect(actual).toMatchObject(e.value as object); + return; + case "equals": + expect(actual).toEqual(e.value); + return; + case "arrayContains": + expect(actual).toEqual(expect.arrayContaining(e.items)); + return; + } +} diff --git a/packages/integration-test-env/package.json b/packages/integration-test-env/package.json index b71cd8eff0..842298d7ff 100644 --- a/packages/integration-test-env/package.json +++ b/packages/integration-test-env/package.json @@ -10,6 +10,7 @@ "typecheck": "tsc --noEmit" }, "dependencies": { + "@ensnode/ens-test-kit": "workspace:*", "@ensnode/shared-configs": "workspace:*", "@ensnode/datasources": "workspace:*", "@ensnode/ensdb-sdk": "workspace:*", diff --git a/packages/integration-test-env/src/adapters/rest-adapter.ts b/packages/integration-test-env/src/adapters/rest-adapter.ts new file mode 100644 index 0000000000..441cad044e --- /dev/null +++ b/packages/integration-test-env/src/adapters/rest-adapter.ts @@ -0,0 +1,76 @@ +import type { ResolutionsApi } from "@ensnode/ens-test-kit/interfaces"; +import type { ChainId, Hex, NormalizedName, RecordsSelection, ResolvedRecords } from "@ensnode/ens-test-kit/types"; + +type ResolveRecordsResponse = { + records: ResolvedRecords; +}; + +type ResolvePrimaryNameResponse = { + name: string | null; +}; + +type ResolvePrimaryNamesResponse = { + names: Record; +}; + +export class RestAdapter implements ResolutionsApi { + constructor(private readonly baseUrl: string) {} + + async resolveRecords(name: NormalizedName, selection: RecordsSelection): Promise { + const query = new URLSearchParams(); + if (selection.name) query.set("name", "true"); + if (selection.addresses && selection.addresses.length > 0) { + query.set("addresses", selection.addresses.join(",")); + } + if (selection.texts && selection.texts.length > 0) { + query.set("texts", selection.texts.join(",")); + } + if (selection.contenthash) query.set("contenthash", "true"); + if (selection.pubkey) query.set("pubkey", "true"); + if (selection.abi) query.set("abi", "1"); + if (selection.interfaceIds && selection.interfaceIds.length > 0) { + query.set("interfaces", selection.interfaceIds.join(",")); + } + + const encodedName = encodeURIComponent(name); + const suffix = query.size > 0 ? `?${query.toString()}` : ""; + const body = await this.fetchJson( + `/api/resolve/records/${encodedName}${suffix}`, + ); + + return body.records; + } + + async resolvePrimaryName(address: Hex, chainId: ChainId): Promise { + const body = await this.fetchJson( + `/api/resolve/primary-name/${address}/${chainId}`, + ); + return body.name; + } + + async resolvePrimaryNames( + address: Hex, + chainIds?: ChainId[], + ): Promise> { + const query = + chainIds && chainIds.length > 0 ? `?chainIds=${encodeURIComponent(chainIds.join(","))}` : ""; + const body = await this.fetchJson( + `/api/resolve/primary-names/${address}${query}`, + ); + + const parsed: Record = {}; + for (const [rawChainId, name] of Object.entries(body.names)) { + parsed[Number(rawChainId)] = name; + } + return parsed; + } + + private async fetchJson(path: string): Promise { + const response = await fetch(`${this.baseUrl}${path}`); + const body = (await response.json()) as TBody; + if (!response.ok) { + throw new Error(`REST adapter request failed (${response.status}) for ${path}`); + } + return body; + } +} diff --git a/packages/integration-test-env/src/tests/resolution-rest.integration.test.ts b/packages/integration-test-env/src/tests/resolution-rest.integration.test.ts new file mode 100644 index 0000000000..c7d832aab7 --- /dev/null +++ b/packages/integration-test-env/src/tests/resolution-rest.integration.test.ts @@ -0,0 +1,8 @@ +import { forwardResolutionCases, reverseResolutionCases } from "@ensnode/ens-test-kit/cases"; +import { runSuite } from "@ensnode/ens-test-kit/vitest"; + +import { RestAdapter } from "../adapters/rest-adapter"; + +const restAdapter = new RestAdapter(process.env.ENSNODE_URL ?? "http://localhost:4334"); + +runSuite(restAdapter, [...forwardResolutionCases, ...reverseResolutionCases]); diff --git a/packages/integration-test-env/vitest.integration.config.ts b/packages/integration-test-env/vitest.integration.config.ts new file mode 100644 index 0000000000..a0417bb3a8 --- /dev/null +++ b/packages/integration-test-env/vitest.integration.config.ts @@ -0,0 +1,7 @@ +import { defineProject } from "vitest/config"; + +export default defineProject({ + test: { + include: ["src/tests/**/*.integration.test.ts"], + }, +}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 03f33e3a30..12d39f38af 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1158,6 +1158,9 @@ importers: '@ensnode/datasources': specifier: workspace:* version: link:../datasources + '@ensnode/ens-test-kit': + specifier: workspace:* + version: link:../ens-test-kit '@ensnode/ensdb-sdk': specifier: workspace:* version: link:../ensdb-sdk From 4d97ae6e40dc0b02abae888390bd5c0ebd268b9e Mon Sep 17 00:00:00 2001 From: sevenzing Date: Fri, 1 May 2026 23:04:02 +0400 Subject: [PATCH 18/18] add llm files --- .memory-bank/tasks/0006-ens-test-kit/IDEA.md | 30 ++- .memory-bank/tasks/0006-ens-test-kit/IMPL.md | 200 +++++++++++++++++++ 2 files changed, 226 insertions(+), 4 deletions(-) create mode 100644 .memory-bank/tasks/0006-ens-test-kit/IMPL.md diff --git a/.memory-bank/tasks/0006-ens-test-kit/IDEA.md b/.memory-bank/tasks/0006-ens-test-kit/IDEA.md index 1dc441d0da..5918c5d824 100644 --- a/.memory-bank/tasks/0006-ens-test-kit/IDEA.md +++ b/.memory-bank/tasks/0006-ens-test-kit/IDEA.md @@ -155,12 +155,32 @@ type TestCase = { description: string; fixtures: Fixture[]; // declarative preconditions call: (api: Api) => Promise; // what to perform over API. should be simple logic - expected: unknown; // partial shape to match against + expected: Expectation | unknown; // plain value → partial-object match; wrap for other matchers }; ``` The case is generic in the API it requires. TypeScript enforces case-vs-adapter compatibility at compile time. No runtime tagging. +`expected` is intentionally plain data — **no Vitest `expect` matchers may appear inside a case**. Embedding `expect.*` would tie the catalog to a test runner and break module-load for any non-Vitest consumer (the `seed` CLI, a docs generator, a future JSON exporter). For asymmetric matching the kit ships a small serialisable DSL: + +```ts +// ens-test-kit/src/cases/expectation.ts +const EXPECTATION = Symbol.for("ens-test-kit.expectation"); + +export type Expectation = + | { [EXPECTATION]: "partial"; value: unknown } + | { [EXPECTATION]: "equals"; value: unknown } + | { [EXPECTATION]: "arrayContains"; items: unknown[] }; + +export const expectation = { + partial: (value: unknown): Expectation => ({ [EXPECTATION]: "partial", value }), + equals: (value: unknown): Expectation => ({ [EXPECTATION]: "equals", value }), + arrayContains: (...items: unknown[]): Expectation => ({ [EXPECTATION]: "arrayContains", items }), +}; +``` + +`runSuite` inspects `expected`: if it's an `Expectation`, it translates to the appropriate Vitest matcher (`toMatchObject` / `toEqual` / `toEqual(expect.arrayContaining(...))`); otherwise it treats the value as a partial-object expectation (the common case) and calls `toMatchObject`. Cases stay framework-agnostic. + ### Example: a resolution case ```ts @@ -228,6 +248,8 @@ A case using both interfaces declares the intersection explicitly: ```ts // ens-test-kit/src/cases/accounts/owned-domains.ts +import { expectation } from "../expectation"; + export const ownershipCases: TestCase[] = [ { id: "accounts.owns.owner-owns-test-eth", @@ -237,7 +259,7 @@ export const ownershipCases: TestCase[] = [ const account = await api.getAccount(OWNER_ADDRESS); return account?.domains.map((d) => d.name); }, - expected: expect.arrayContaining(["test.eth"]), + expected: expectation.arrayContains("test.eth"), }, ]; ``` @@ -303,7 +325,7 @@ const HANDLERS = { export async function seedFixtures(rpcUrl: string, fixtures: Fixture[]): Promise { const ctx = createSeederContext(rpcUrl); - const deduped = dedupeFixtures(fixtures); // by id + const deduped = dedupeFixtures(fixtures); // by id; throws if same id has unequal content const ordered = topologicallySort(deduped); // registrations before records, etc. for (const fixture of ordered) { const handler = HANDLERS[fixture.kind]; @@ -312,7 +334,7 @@ export async function seedFixtures(rpcUrl: string, fixtures: Fixture[]): Promise } ``` -The seeder is idempotent at the fixture level (same `id` → applied once). Topological ordering handles dependencies (you must register `parent.eth` before setting records on `sub.parent.eth`). +The seeder is idempotent at the fixture level: a given `id` is applied at most once. `dedupeFixtures` enforces *content consistency* — if two cases contribute a fixture with the same `id` but different fields, the seeder throws at startup rather than silently dropping one and making a downstream case test the wrong on-chain state. Same id + same content → safe reuse; same id + different content → developer error, fail loud. Topological ordering handles dependencies (you must register `parent.eth` before setting records on `sub.parent.eth`). ## How seeding plugs into Docker diff --git a/.memory-bank/tasks/0006-ens-test-kit/IMPL.md b/.memory-bank/tasks/0006-ens-test-kit/IMPL.md new file mode 100644 index 0000000000..6685f14ee2 --- /dev/null +++ b/.memory-bank/tasks/0006-ens-test-kit/IMPL.md @@ -0,0 +1,200 @@ +# Task 0006: `@ensnode/ens-test-kit` — Implementation Steps + +See [IDEA.md](./IDEA.md) for the design rationale. + +**Steps, not PRs.** This document is broken into implementation *steps*. How these steps map to pull requests is **the user's decision** — some steps may ship as a single PR, others may be combined, and a single step may be split across multiple PRs if it grows. The user will announce the PR boundary (e.g. "ship a PR now after step 2", "combine steps 3 and 4 into one PR") as work progresses. Do not open a PR per step automatically. + +Steps are ordered by dependency: later steps assume earlier steps are in place. Each step ends with an **Acceptance** checklist that must hold before moving on. + +Implementation preference: if an existing tool/library already solves the problem (argument parsing, sorting, data ops, etc.), use it instead of writing custom code. +Implementation quality: avoid repetition (DRY). Shared logic and tunable constants (timeouts, confirmations, polling intervals, etc.) must be centralized behind one helper/config instead of duplicated across handlers. + +--- + +## Step 1 — Skeleton package + interfaces + types + +**Goal:** ship the package shell with the contract types, no runtime behavior yet. + +**Scope:** +- Create `packages/ens-test-kit/` with `package.json`, `tsconfig.json`, `vitest.config.ts`, `tsup.config.ts`, `README.md`. +- Add to monorepo: `pnpm-workspace.yaml`, root `tsconfig` references if applicable, `biome.json` if needed. +- Define `src/interfaces/`: + - `resolutions.ts` — `ResolutionsApi` + - `domains.ts` — `DomainsApi` + - `accounts.ts` — `AccountsApi` + - `resolvers.ts` — `ResolversApi` +- Define `src/types/`: + - `Domain`, `Account`, `Resolver`, `Registration`, `Connection`, `DomainsWhere`, `RecordsSelection`, `ResolvedRecords`, `NormalizedName`, `Hex`, `ChainId`, `ResolverId`. +- Define `src/seeder/types.ts`: + - `Fixture` discriminated union (re-exported from per-fixture files), `FixtureKind`, `FixtureBase`, `SeederContext`. +- Define `src/cases/types.ts`: + - `TestCase`. +- Public exports via subpath: `interfaces`, `types`, `cases`, `seeder`, `vitest`, `cli`. + +**Acceptance:** package builds, exports type-check, no runtime code yet. + +--- + +## Step 2 — Fixtures + seeder runtime + `seed` CLI + seeded devnet image + +**Goal:** the kit owns all seeding, top to bottom; the devnet container seeds itself on startup; orchestrator no longer seeds. + +**Scope:** + +*Fixtures (one file per fixture kind, each exporting type + builder + handler):* +- `src/seeder/fixtures/reverse-name.ts` — `ReverseNameFixture`, `reverseName(args)`, `applyReverseNameFixture(fixture, ctx)`. +- `src/seeder/fixtures/text-record.ts` +- `src/seeder/fixtures/multicoin-address.ts` +- `src/seeder/fixtures/contenthash.ts` +- `src/seeder/fixtures/pubkey.ts` +- `src/seeder/fixtures/abi.ts` +- `src/seeder/fixtures/interface-record.ts` +- Each handler ports the existing logic from [packages/integration-test-env/src/seed/primary-names.ts](packages/integration-test-env/src/seed/primary-names.ts) and [packages/integration-test-env/src/seed/resolver-records.ts](packages/integration-test-env/src/seed/resolver-records.ts). + +*Seeder runtime:* +- `src/seeder/index.ts`: + - `createSeederContext(rpcUrl)` — wallet clients (deployer, owner, user, user2). + - `seedFixtures(rpcUrl, fixtures)` — dedupe by id, preserve fixture input order, dispatch to handlers. + - `dedupeFixtures` **must throw** when two fixtures share an `id` but are not deeply equal (use structural equality on fixture fields, ignoring object identity). Silent dedup is disallowed — it would let a second case override a first case's on-chain state and pass against the wrong fixture. Include both fixtures' JSON in the thrown error message for fast diagnosis. +- `src/seeder/fixtures/common.ts`: + - `canonicalFixtures` — the union of fixtures that match what's seeded today. + +*CLI:* +- `src/cli/seed.ts`: parses `--rpc ` and `--fixtures ` (defaults to canonical) using an existing parser utility/library, calls `seedFixtures`, prints a readable per-name summary on success. +- `bin/ens-test-kit` declared in `package.json#bin`. Single subcommand: `seed`. No `up`. + +*Docker image:* +- `packages/ens-test-kit/devnet/Dockerfile` — multi-stage: + - Build stage: pull workspace lockfile + kit source from monorepo build context, run `pnpm -F ens-test-kit build`. + - Runtime stage: `FROM ghcr.io/ensdomains/contracts-v2:main-9f26a8f`, install Node, copy in built kit + `entrypoint.sh`. +- `packages/ens-test-kit/devnet/entrypoint.sh`: + - Start `./script/runDevnet.ts --testNames` in the background. + - Wait until Anvil JSON-RPC at `localhost:8545` is responsive. + - Run `node /opt/ens-test-kit/cli.js seed --rpc http://localhost:8545`. + - Only after seeding succeeds, expose the contracts-v2 health endpoint as healthy. Implementation hook: contracts-v2's `runDevnet.ts` already serves `/health` on `:8000`; either hold a small proxy in front of it that returns 503 until seeding completes, or set a sentinel file the existing `/health` checker reads. Decide during implementation; flag for review. + - `wait` on the devnet process so the container stays up. +- Update [docker/services/devnet.yml](docker/services/devnet.yml): + - Replace `image: ghcr.io/ensdomains/contracts-v2:main-9f26a8f` with `build: { context: ../.., dockerfile: packages/ens-test-kit/devnet/Dockerfile }`. + - Keep healthcheck config; semantics are now "anvil up *and* seeded". + - Once we publish a tagged image of this Dockerfile (out of scope here), revert to `image:` with that tag. + +*Orchestrator:* +- Remove the `seedDevnet()` call from [packages/integration-test-env/src/orchestrator.ts](packages/integration-test-env/src/orchestrator.ts) phase 2. The container's `service_healthy` wait is now sufficient. +- Delete now-unused [packages/integration-test-env/src/seed/primary-names.ts](packages/integration-test-env/src/seed/primary-names.ts), [packages/integration-test-env/src/seed/resolver-records.ts](packages/integration-test-env/src/seed/resolver-records.ts), [packages/integration-test-env/src/seed/index.ts](packages/integration-test-env/src/seed/index.ts). + +**Acceptance:** +- `docker compose -f docker/services/devnet.yml up devnet` produces a seeded chain reachable at `localhost:8545`. `cast call` against the resolver returns the expected records; `cast call` against the reverse resolver returns the expected primary name. +- `pnpm -F integration-test-env start` produces identical end-to-end behavior; existing integration tests pass without modification. +- Devnet container's healthcheck only flips green after seeding completes (verifiable by tailing entrypoint logs vs `docker inspect`'s `Health.Status` transitions). + +--- + +## Step 3 — Test case framework + first port (resolution via REST) + +**Goal:** prove the case-and-runner abstraction end-to-end with one concern. + +**Scope:** +- Implement `src/cases/expectation.ts` — `Expectation` discriminated union + `expectation.{partial,equals,arrayContains}` builders (see [IDEA.md](./IDEA.md#a-testcase-is-just-data)). `EXPECTATION` sentinel is a `Symbol.for(...)` so the tag survives module-boundary crossing in workspaces. +- Implement `src/vitest/run-suite.ts` — `runSuite(adapter, cases)`: + ```ts + import { describe, it, expect } from "vitest"; + import { type Expectation, isExpectation } from "../cases/expectation"; + + export function runSuite(adapter: Api, cases: TestCase[]): void { + describe(`suite (${cases.length} cases)`, () => { + it.each(cases)("$id — $description", async (tc) => { + const actual = await tc.call(adapter); + if (isExpectation(tc.expected)) { + assertExpectation(actual, tc.expected); + } else { + expect(actual).toMatchObject(tc.expected as object); + } + }); + }); + } + + function assertExpectation(actual: unknown, e: Expectation): void { + switch (e[EXPECTATION]) { + case "partial": expect(actual).toMatchObject(e.value as object); return; + case "equals": expect(actual).toEqual(e.value); return; + case "arrayContains": expect(actual).toEqual(expect.arrayContaining(e.items)); return; + } + } + ``` + Cases never import Vitest. The translation from the data DSL to Vitest matchers happens only here, inside the runner. +- Add a lint check (or unit test) that scans the `src/cases/` tree and fails if any file imports `vitest` or references `expect.` — the "no Vitest in cases" invariant is worth enforcing mechanically. +- Implement `src/cases/resolution/forward.ts` and `reverse.ts` — port positive-path cases from: + - [resolve-records.integration.test.ts](apps/ensapi/src/handlers/api/resolution/resolve-records.integration.test.ts) (positive paths) + - [resolve-primary-name.integration.test.ts](apps/ensapi/src/handlers/api/resolution/resolve-primary-name.integration.test.ts) (positive paths) + - [resolve-primary-names.integration.test.ts](apps/ensapi/src/handlers/api/resolution/resolve-primary-names.integration.test.ts) (positive paths) + - Cases derive `expected` values from referenced fixtures. +- Add `RestAdapter` in `packages/integration-test-env/src/adapters/rest-adapter.ts` implementing `ResolutionsApi`. +- Add `packages/integration-test-env/src/tests/resolution-rest.integration.test.ts` calling `runSuite(restAdapter, [...forwardResolutionCases, ...reverseResolutionCases])`. +- Trim ported cases out of the original `apps/ensapi/src/handlers/api/resolution/*.integration.test.ts` files; **keep** the validation/400 cases there (they're transport-specific). + +**Acceptance:** new tests pass against running ENSNode; old test files reduced to validation-only with all positive paths now driven by the kit; CI green. + +--- + +## Step 4 — Domains, Accounts, Resolvers cases via Omnigraph adapter + +**Goal:** widen the kit beyond resolution to cover the omnigraph surface. + +**Scope:** +- Implement `OmnigraphAdapter` in `packages/integration-test-env/src/adapters/omnigraph-adapter.ts`: + - One method per interface method. + - Hand-written GraphQL queries (codegen optional, defer if straightforward). + - Implements `DomainsApi`, `AccountsApi`, `ResolversApi`. +- Build initial cases: + - `src/cases/domains/by-name.ts` — getDomainByName for known seeded names. + - `src/cases/domains/subdomains.ts` — parent/sub relationships from existing devnet (parent.eth, sub.parent.eth, sub1.sub2.parent.eth). + - `src/cases/domains/listing.ts` — `listDomains` with where clauses. + - `src/cases/accounts/owned-domains.ts` — owner account's domain list (uses `DomainsApi & AccountsApi`). + - `src/cases/resolvers/indexed-records.ts` — `listResolverRecords` returns the keys/coinTypes set on test.eth. +- Add per-concern test files in `packages/integration-test-env/src/tests/`. + +**Acceptance:** four new test files run cases through `OmnigraphAdapter`; existing test files unchanged. + +--- + +## Step 5 — Backfill missing scenarios + +**Goal:** close the coverage gaps documented in [Task 0004](.memory-bank/tasks/0004-ensnode-tests/PLAN.md). + +**Scope (each item adds fixtures + cases; some require contracts-v2 capability checks):** +- Wildcard resolver scenario. +- Wrapped name (NameWrapper). +- Expired name (use `evm_setNextBlockTimestamp` in seeder, or contracts-v2 helper if available). +- Multi-coin reverse resolvers (Base, Linea) on owner address. +- Custom CCIP/offchain resolver (if devnet supports — investigate during implementation; otherwise punt to its own task). +- ENSv1 vs ENSv2 reverse resolver variants for the same address. + +Cases are added file-by-file per scenario; fixtures gain new builders as needed. Each scenario claims its own name to avoid collisions. + +**Acceptance:** new cases pass against the omnigraph and (where applicable) the REST adapter; documentation in the kit's README enumerates available fixture types and seeded names. + +--- + +## Step 6 — Resolution adapter for omnigraph (after [Task 0003](.memory-bank/tasks/0003-omnigraph-resolution-api/PLAN.md)) + +**Goal:** once omnigraph exposes resolution, run the same resolution cases through it for free. + +**Scope:** +- `OmnigraphAdapter` additionally implements `ResolutionsApi`. +- Add `packages/integration-test-env/src/tests/resolution-omnigraph.integration.test.ts` running the same case set through `OmnigraphAdapter`. +- Cases are unchanged. The same `forwardResolutionCases` array now exercises both REST and GraphQL. + +**Acceptance:** one source of resolution truth, two transports validated; any divergence between REST and GraphQL surfaces as a test failure. + +--- + +## Definition of done for the proposal + +This plan is ready for implementation when: +- The two-package shape is approved (`ens-test-kit` + slimmed `integration-test-env`). +- The interface segregation (4 narrow interfaces) is approved. +- One-file-per-fixture-kind layout (declaration + builder + handler combined) is approved. +- "Over-fetch in V1, no `select`" is approved. +- The Docker-side seeding model (devnet image self-seeds; orchestrator doesn't seed; no kit `up` command) is approved. +- The `seed` CLI as the kit's single binary entry point is approved. +- The step sequence is approved (or a different sequencing is agreed).