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 d126cd16e5..bd28d9ffa5 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,51 +6,61 @@ import { describe, expect, it } from "vitest"; -import { DevnetAccounts } from "@ensnode/ensnode-sdk/internal"; +import { accounts } from "@ensnode/datasources/devnet"; 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: DevnetAccounts.owner.address, + description: "resolves primary name for owner address on chain 1", + address: accounts.owner.address, chainId: "1", query: "", - expectedStatus: 200, - expectedBody: { name: null, accelerationRequested: false, accelerationAttempted: false }, + expected: { + status: 200, + body: { + name: "test.eth", + accelerationRequested: false, + accelerationAttempted: false, + }, + }, }, { - description: - "resolves primary name for user address on chain 1 (no primary name set in devnet)", - address: DevnetAccounts.user.address, + 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 }, + expected: { + status: 200, + body: { name: null, accelerationRequested: false, accelerationAttempted: false }, + }, }, { description: "owner address with accelerate=true returns accelerationRequested: true", - address: DevnetAccounts.owner.address, + address: accounts.owner.address, chainId: "1", query: "accelerate=true", - expectedStatus: 200, - expectedBody: { accelerationRequested: true, accelerationAttempted: false }, + expected: { + status: 200, + body: { accelerationRequested: true, accelerationAttempted: false }, + }, }, { description: "returns 400 for invalid (non-hex) address", address: "notanaddress", chainId: "1", query: "", - expectedStatus: 400, - expectedBody: { - message: "Invalid Input", - details: { - errors: [], - properties: { - address: { - errors: ["EVM address must be a valid EVM address"], + expected: { + status: 400, + body: { + message: "Invalid Input", + details: { + errors: [], + properties: { + address: { + errors: ["EVM address must be a valid EVM address"], + }, }, }, }, @@ -58,29 +68,33 @@ describe("GET /api/resolve/primary-name/:address/:chainId", () => { }, { description: "returns 400 for non-numeric chainId", - address: DevnetAccounts.owner.address, + address: accounts.owner.address, chainId: "notachainid", query: "", - expectedStatus: 400, - expectedBody: { - message: "Invalid Input", - details: { - errors: [], - properties: { - chainId: { - errors: ["Defaultable Chain ID String must represent a non-negative integer (>=0)."], + expected: { + status: 400, + body: { + message: "Invalid Input", + details: { + errors: [], + properties: { + chainId: { + errors: [ + "Defaultable Chain ID String must represent a non-negative integer (>=0).", + ], + }, }, }, }, }, }, - ])("$description", async ({ address, chainId, query, expectedStatus, expectedBody }) => { + ])("$description", async ({ address, chainId, query, expected }) => { const response = await fetch( `${BASE_URL}/api/resolve/primary-name/${address}/${chainId}${query ? `?${query}` : ""}`, ); const body = await response.json(); - expect(response.status).toBe(expectedStatus); - expect(body).toMatchObject(expectedBody); + expect(response.status).toBe(expected.status); + expect(body).toMatchObject(expected.body); }); }); 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 3f95be0504..07f7e109e8 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,47 +6,52 @@ import { describe, expect, it } from "vitest"; -import { DevnetAccounts } from "@ensnode/ensnode-sdk/internal"; +import { accounts } from "@ensnode/datasources/devnet"; 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: DevnetAccounts.owner.address, + description: "resolves primary names for owner address on chain 1", + address: accounts.owner.address, query: "chainIds=1", - expectedStatus: 200, - expectedBody: { - names: { "1": null }, - accelerationRequested: false, - accelerationAttempted: false, + expected: { + status: 200, + body: { + names: { "1": "test.eth" }, + accelerationRequested: false, + accelerationAttempted: false, + }, }, }, { description: "resolves all primary names", - address: DevnetAccounts.owner.address, + address: accounts.owner.address, query: "", - expectedStatus: 200, - expectedBody: { - names: { "1": null }, - accelerationRequested: false, - accelerationAttempted: false, + expected: { + status: 200, + body: { + names: { "1": "test.eth" }, + accelerationRequested: false, + accelerationAttempted: false, + }, }, }, { description: "returns 400 for invalid (non-hex) address", address: "notanaddress", query: "chainIds=1", - expectedStatus: 400, - expectedBody: { - message: "Invalid Input", - details: { - errors: [], - properties: { - address: { - errors: ["EVM address must be a valid EVM address"], + expected: { + status: 400, + body: { + message: "Invalid Input", + details: { + errors: [], + properties: { + address: { + errors: ["EVM address must be a valid EVM address"], + }, }, }, }, @@ -54,21 +59,23 @@ describe("GET /api/resolve/primary-names/:address", () => { }, { description: "returns 400 when chainIds contains the default chain id (0)", - address: DevnetAccounts.owner.address, + address: accounts.owner.address, query: "chainIds=0", - expectedStatus: 400, - expectedBody: { - message: "Invalid Input", - details: { - errors: [], - properties: { - chainIds: { - errors: [], - items: [ - { - errors: ["Must not be the 'default' EVM chain id (0)."], - }, - ], + expected: { + status: 400, + body: { + message: "Invalid Input", + details: { + errors: [], + properties: { + chainIds: { + errors: [], + items: [ + { + errors: ["Must not be the 'default' EVM chain id (0)."], + }, + ], + }, }, }, }, @@ -76,28 +83,30 @@ describe("GET /api/resolve/primary-names/:address", () => { }, { description: "returns 400 when chainIds contains duplicate chain ids", - address: DevnetAccounts.owner.address, + address: accounts.owner.address, query: "chainIds=1,1", - expectedStatus: 400, - expectedBody: { - message: "Invalid Input", - details: { - errors: [], - properties: { - chainIds: { - errors: ["Must be a set of unique entries."], + expected: { + status: 400, + body: { + message: "Invalid Input", + details: { + errors: [], + properties: { + chainIds: { + errors: ["Must be a set of unique entries."], + }, }, }, }, }, }, - ])("$description", async ({ address, query, expectedStatus, expectedBody }) => { + ])("$description", async ({ address, query, expected }) => { const response = await fetch( `${BASE_URL}/api/resolve/primary-names/${address}${query ? `?${query}` : ""}`, ); const body = await response.json(); - expect(response.status).toBe(expectedStatus); - expect(body).toMatchObject(expectedBody); + expect(response.status).toBe(expected.status); + expect(body).toMatchObject(expected.body); }); }); 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 9f6c500dcb..5d49aa8682 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 { DevnetAccounts } from "@ensnode/ensnode-sdk/internal"; +import { accounts, addresses, fixtures } from "@ensnode/datasources/devnet"; const BASE_URL = process.env.ENSNODE_URL!; @@ -16,11 +16,13 @@ describe("GET /api/resolve/records/:name", () => { description: "resolves ETH address (coin 60) for test.eth", name: "test.eth", query: "addresses=60", - expectedStatus: 200, - expectedBody: { - records: { addresses: { 60: DevnetAccounts.owner.address } }, - accelerationRequested: false, - accelerationAttempted: false, + expected: { + status: 200, + body: { + records: { addresses: { 60: accounts.owner.address } }, + accelerationRequested: false, + accelerationAttempted: false, + }, }, }, { @@ -28,22 +30,26 @@ describe("GET /api/resolve/records/:name", () => { "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: DevnetAccounts.owner.address } }, - accelerationRequested: false, - accelerationAttempted: false, + expected: { + status: 200, + body: { + 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, + expected: { + status: 200, + body: { + records: { texts: { description: "example.eth" } }, + accelerationRequested: false, + accelerationAttempted: false, + }, }, }, { @@ -51,36 +57,42 @@ describe("GET /api/resolve/records/:name", () => { "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, + expected: { + status: 200, + body: { + 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: DevnetAccounts.owner.address }, - texts: { description: "example.eth" }, + expected: { + status: 200, + body: { + records: { + addresses: { 60: accounts.owner.address }, + texts: { description: "example.eth" }, + }, + accelerationRequested: false, + accelerationAttempted: false, }, - 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, + expected: { + status: 200, + body: { + records: { addresses: { 60: null } }, + accelerationRequested: false, + accelerationAttempted: false, + }, }, }, { @@ -88,33 +100,39 @@ describe("GET /api/resolve/records/:name", () => { "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: DevnetAccounts.owner.address } }, - accelerationRequested: false, - accelerationAttempted: false, + expected: { + status: 200, + body: { + 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, + expected: { + status: 200, + body: { + 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: DevnetAccounts.owner.address } }, - accelerationRequested: false, - accelerationAttempted: false, + expected: { + status: 200, + body: { + records: { addresses: { 60: accounts.owner.address } }, + accelerationRequested: false, + accelerationAttempted: false, + }, }, }, { @@ -122,46 +140,153 @@ describe("GET /api/resolve/records/:name", () => { "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: DevnetAccounts.owner.address } }, - accelerationRequested: false, - accelerationAttempted: false, + expected: { + status: 200, + body: { + 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", + expected: { + status: 200, + body: { + 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", + expected: { + status: 200, + body: { + 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", + expected: { + status: 200, + body: { + 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("&"), + expected: { + status: 200, + body: { + 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: DevnetAccounts.owner.address } }, - accelerationRequested: true, - accelerationAttempted: false, + expected: { + status: 200, + body: { + 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", query: "", - expectedStatus: 400, - expectedBody: { - message: "Invalid Input", - details: { errors: ["Selection cannot be empty."] }, + expected: { + status: 400, + body: { + message: "Invalid Input", + details: { errors: ["Selection cannot be empty."] }, + }, }, }, { description: "returns 400 when name is not normalized (uppercase)", name: "TEST.ETH", query: "addresses=60", - expectedStatus: 400, - expectedBody: { - message: "Invalid Input", - details: { - errors: [], - properties: { - name: { - errors: ["Must be normalized, see https://docs.ens.domains/resolution/names/"], + expected: { + status: 400, + body: { + message: "Invalid Input", + details: { + errors: [], + properties: { + name: { + errors: ["Must be normalized, see https://docs.ens.domains/resolution/names/"], + }, }, }, }, @@ -171,19 +296,21 @@ describe("GET /api/resolve/records/:name", () => { description: "returns 400 when addresses contains a non-numeric coin type", name: "test.eth", query: "addresses=notacointype", - expectedStatus: 400, - expectedBody: { - message: "Invalid Input", - details: { - errors: [], - properties: { - addresses: { - errors: [], - items: [ - { - errors: ["Coin Type String must represent a non-negative integer (>=0)."], - }, - ], + expected: { + status: 400, + body: { + message: "Invalid Input", + details: { + errors: [], + properties: { + addresses: { + errors: [], + items: [ + { + errors: ["Coin Type String must represent a non-negative integer (>=0)."], + }, + ], + }, }, }, }, @@ -193,14 +320,16 @@ describe("GET /api/resolve/records/:name", () => { description: "returns 400 when addresses contains duplicate coin types", name: "test.eth", query: "addresses=60,60", - expectedStatus: 400, - expectedBody: { - message: "Invalid Input", - details: { - errors: [], - properties: { - addresses: { - errors: ["Must be a set of unique entries."], + expected: { + status: 400, + body: { + message: "Invalid Input", + details: { + errors: [], + properties: { + addresses: { + errors: ["Must be a set of unique entries."], + }, }, }, }, @@ -210,27 +339,29 @@ describe("GET /api/resolve/records/:name", () => { description: "returns 400 when texts contains duplicate keys", name: "test.eth", query: "texts=avatar,avatar", - expectedStatus: 400, - expectedBody: { - message: "Invalid Input", - details: { - errors: [], - properties: { - texts: { - errors: ["Must be a set of unique entries."], + expected: { + status: 400, + body: { + message: "Invalid Input", + details: { + errors: [], + properties: { + texts: { + errors: ["Must be a set of unique entries."], + }, }, }, }, }, }, - ])("$description", async ({ name, query, expectedStatus, expectedBody }) => { + ])("$description", async ({ name, query, expected }) => { const encodedName = encodeURIComponent(name); const response = await fetch( `${BASE_URL}/api/resolve/records/${encodedName}${query ? `?${query}` : ""}`, ); const body = await response.json(); - expect(response.status).toBe(expectedStatus); - expect(body).toMatchObject(expectedBody); + expect(response.status).toBe(expected.status); + expect(body).toMatchObject(expected.body); }); }); 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 a7146e12f1..50f3a1d370 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 { DevnetAccounts } from "@ensnode/ensnode-sdk/internal"; +import { accounts } from "@ensnode/datasources/devnet"; import { AccountDomainsPaginated, @@ -47,7 +47,7 @@ describe("Account.domains", () => { it("returns domains owned by the devnet owner", async () => { const result = await request(AccountDomains, { - address: DevnetAccounts.owner.address, + address: accounts.owner.address, }); const domains = flattenConnection(result.account.domains); const names = domains.map((d) => d.name); @@ -74,7 +74,7 @@ describe("Account.domains", () => { it("returns domains owned by the new owner", async () => { const result = await request(AccountDomains, { - address: DevnetAccounts.user.address, + address: accounts.user.address, }); const domains = flattenConnection(result.account.domains); const names = domains.map((d) => d.name); @@ -85,7 +85,7 @@ describe("Account.domains", () => { describe("version?: ENSProtocolVersion", () => { it("returns any version when unspecified", async () => { const result = await request(AccountDomains, { - address: DevnetAccounts.owner.address, + address: accounts.owner.address, version: undefined, }); const domains = flattenConnection(result.account.domains); @@ -95,7 +95,7 @@ describe("Account.domains", () => { it("returns only ENSv1Domains when version: ENSv1", async () => { const result = await request(AccountDomains, { - address: DevnetAccounts.owner.address, + address: accounts.owner.address, version: "ENSv1", }); const domains = flattenConnection(result.account.domains); @@ -105,7 +105,7 @@ describe("Account.domains", () => { it("returns only ENSv2Domains when version: ENSv2", async () => { const result = await request(AccountDomains, { - address: DevnetAccounts.owner.address, + address: accounts.owner.address, version: "ENSv2", }); const domains = flattenConnection(result.account.domains); @@ -119,7 +119,7 @@ describe("Account.domains pagination", () => { testDomainPagination(async (variables) => { const result = await request<{ account: { domains: PaginatedGraphQLConnection }; - }>(AccountDomainsPaginated, { address: DevnetAccounts.owner.address, ...variables }); + }>(AccountDomainsPaginated, { address: accounts.owner.address, ...variables }); return result.account.domains; }); }); @@ -136,14 +136,14 @@ describe("Account.events", () => { it("returns events for the devnet deployer", async () => { const result = await request(AccountEvents, { - address: DevnetAccounts.deployer.address, + address: accounts.deployer.address, }); const events = flattenConnection(result.account.events); expect(events.length).toBeGreaterThan(0); for (const event of events) { - expect(event.sender).toBe(DevnetAccounts.deployer.address); + expect(event.sender).toBe(accounts.deployer.address); } }); }); @@ -152,7 +152,7 @@ describe("Account.events pagination", () => { testEventPagination(async (variables) => { const result = await request<{ account: { events: PaginatedGraphQLConnection }; - }>(AccountEventsPaginated, { address: DevnetAccounts.deployer.address, ...variables }); + }>(AccountEventsPaginated, { address: accounts.deployer.address, ...variables }); return result.account.events; }); }); @@ -171,7 +171,7 @@ describe("Account.events filtering (AccountEventsWhereInput)", () => { beforeAll(async () => { const result = await request(AccountEventsFiltered, { - address: DevnetAccounts.deployer.address, + address: accounts.deployer.address, first: 1000, }); // events are returned in ascending order, so first/last access yields min/max values @@ -183,7 +183,7 @@ describe("Account.events filtering (AccountEventsWhereInput)", () => { const targetSelector = allEvents[0].topics[0]; const result = await request(AccountEventsFiltered, { - address: DevnetAccounts.deployer.address, + address: accounts.deployer.address, where: { selector_in: [targetSelector] }, }); const events = flattenConnection(result.account.events); @@ -196,7 +196,7 @@ describe("Account.events filtering (AccountEventsWhereInput)", () => { it("filters by selector_in with unknown topic returns no results", async () => { const result = await request(AccountEventsFiltered, { - address: DevnetAccounts.deployer.address, + address: accounts.deployer.address, where: { selector_in: ["0x0000000000000000000000000000000000000000000000000000000000000001"], }, @@ -207,7 +207,7 @@ describe("Account.events filtering (AccountEventsWhereInput)", () => { it("filters by empty selector_in returns no results", async () => { const result = await request(AccountEventsFiltered, { - address: DevnetAccounts.deployer.address, + address: accounts.deployer.address, where: { selector_in: [] }, }); const events = flattenConnection(result.account.events); @@ -218,7 +218,7 @@ describe("Account.events filtering (AccountEventsWhereInput)", () => { const midTimestamp = allEvents[Math.floor(allEvents.length / 2)].timestamp; const result = await request(AccountEventsFiltered, { - address: DevnetAccounts.deployer.address, + address: accounts.deployer.address, where: { timestamp_gte: midTimestamp }, }); const events = flattenConnection(result.account.events); @@ -234,7 +234,7 @@ describe("Account.events filtering (AccountEventsWhereInput)", () => { const midTimestamp = allEvents[Math.floor(allEvents.length / 2)].timestamp; const result = await request(AccountEventsFiltered, { - address: DevnetAccounts.deployer.address, + address: accounts.deployer.address, where: { timestamp_lte: midTimestamp }, }); const events = flattenConnection(result.account.events); @@ -251,7 +251,7 @@ describe("Account.events filtering (AccountEventsWhereInput)", () => { const maxTs = allEvents[allEvents.length - 1].timestamp; const result = await request(AccountEventsFiltered, { - address: DevnetAccounts.deployer.address, + address: accounts.deployer.address, where: { timestamp_gte: minTs, timestamp_lte: maxTs }, first: 1000, }); @@ -268,7 +268,7 @@ describe("Account.events filtering (AccountEventsWhereInput)", () => { const midTimestamp = seedEvent.timestamp; const result = await request(AccountEventsFiltered, { - address: DevnetAccounts.deployer.address, + address: accounts.deployer.address, where: { selector_in: [targetSelector], timestamp_gte: midTimestamp }, }); const events = flattenConnection(result.account.events); @@ -285,7 +285,7 @@ describe("Account.events filtering (AccountEventsWhereInput)", () => { const maxTimestamp = BigInt(allEvents[allEvents.length - 1].timestamp); const result = await request(AccountEventsFiltered, { - address: DevnetAccounts.deployer.address, + address: accounts.deployer.address, where: { timestamp_gte: (maxTimestamp + 1n).toString() }, }); const events = flattenConnection(result.account.events); diff --git a/package.json b/package.json index 01740cd677..eb507994be 100644 --- a/package.json +++ b/package.json @@ -18,7 +18,7 @@ "changeset:publish:next": "changeset publish --no-git-tag --snapshot --tag next", "release:postversion": "pnpm docker:version:sync && pnpm generate:openapi", "packages:prepublish": "pnpm -r prepublish", - "devnet": "docker compose -f docker/docker-compose.devnet.yml up devnet", + "devnet": "docker compose -f docker/services/devnet.yml up", "docker:build:ensnode": "pnpm run -w --parallel \"/^docker:build:.*/\"", "docker:version:sync": "node ./scripts/sync-docker-services-tags.mjs \"$(pnpm -F ensapi -s version:current)\"", "docker:build:ensindexer": "docker build -f apps/ensindexer/Dockerfile -t ghcr.io/namehash/ensnode/ensindexer:latest .", diff --git a/packages/datasources/package.json b/packages/datasources/package.json index 529076aaa1..ab8ccd1905 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", @@ -49,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 new file mode 100644 index 0000000000..1008447288 --- /dev/null +++ b/packages/datasources/src/devnet/constants.ts @@ -0,0 +1,139 @@ +import type { NormalizedAddress } from "enssdk"; +import { asNormalizedAddress, toNormalizedAddress } from "enssdk"; +import type { Hex } from "viem"; +import { mnemonicToAccount } from "viem/accounts"; + +/** + * Deterministic contract addresses for the ENS contracts-v2 devnet used by ens-test-env. + * Keys use the same PascalCase names as the contracts-v2 contract table output. + * Use `pnpm devnet` to see actual data in devnet + * + * @see docker/services/devnet.yml + */ +export const contracts = { + // -- DNS -- + DNSSECGatewayProvider: "0x5fbdb2315678afecb367f032d93f642f64180aa3", + DNSTXTResolver: "0xe7f1725e7734ce288f8367e1bb143e90bb3f0512", + DNSAliasResolver: "0x322813fd9a801c5507c9de605d63cea4f2ce6c44", + DNSTLDResolver: "0x998abeb3e57409262ae5b751f60747921b33613e", + OffchainDNSResolver: "0x851356ae760d987e095750cceb3bc6014560891c", + SimplePublicSuffixList: "0xf5059a5d33d5853360d16c683c16e67980206f36", + DNSRegistrar: "0xf4b146fba71f41e0592668ffbf264f1d186b2ca8", + ExtendedDNSResolver: "0x86a2ee8faf9a840f7a2c64ca3d51209f9a02081d", + + // -- Registries -- + LegacyENSRegistry: "0x9fe46736679d2d9a65f0992f2272de9f3c7fa6e0", + ENSRegistry: "0xcf7ed3acca5a467e9e704c703e8d87f634fb0fc9", + RootRegistry: "0x2279b7a0a67db372996a5fab50d91eaa73d2ebe6", + ETHRegistry: "0x8f86403a4de0bb5791fa46b8e795c547942fe4cf", + ReverseRegistry: "0xcd8a1c3ba11cf5ecfa6267617243239504a98d90", + + // -- Registrars & Controllers -- + BaseRegistrarImplementation: "0x8a791620dd6260079bf849dc5567adc3f2fdc318", + ETHRegistrar: "0x21df544947ba3e8b3c32561399e88b52dc8b2823", + LegacyETHRegistrarController: "0x46b142dd1e924fab83ecc3c08e4d46e82f005e0e", + WrappedETHRegistrarController: "0x253553366da8546fc250f225fe3d25d0c782303b", + ETHRegistrarController: "0x367761085bf3c12e5da2df99ac6e1a824612b8fb", + BatchRegistrar: "0xdc11f7e700a4c898ae5caddb1082cffa76512add", + + // -- Reverse Resolution -- + ETHReverseRegistrar: "0x59b670e9fa9d0a427751af201d676719a970857b", + DefaultReverseRegistrar: "0x4c5859f0f772848b2d91f1d83e2fe57935348029", + DefaultReverseResolver: "0x5f3f1dbd7b74c6b46e8c44f98792a1daf8d69154", + ETHReverseResolver: "0x7bc06c482dead17c0e297afbc32f6e63d3846650", + ReverseRegistrar: "0x162a433068f51e18b7d13932f27e66a3f99e6890", + L2ReverseRegistrar: "0x4631bcabd6df18d94796344963cb60d44a4136b6", + + // -- Resolvers -- + ENSV1Resolver: "0x5fc8d32690cc91d4c39d9d3abcbd16989f875707", + ENSV2Resolver: "0xc6e7df5e7b4f2a278906862b61205850344d4e7d", + OwnedResolver: "0x68b1d87f95878fe05b998f19b66f4baba5de1aed", + PermissionedResolver: "0x5ea90acf6555276660760fe629d72932c91f4b8e", + LegacyPublicResolver: "0xa4899d35897033b927acfcf422bc745916139776", + PublicResolver: "0xf953b3a269d80e3eb0f2947630da976b896a8c5b", + PermissionedResolverImpl: "0x809d550fca64d94bd9f66e60752a544199cfac3d", + UniversalResolver: "0xaa292e8611adf267e563f334ee42320ac96d0463", + UniversalResolverV2: "0x0355b7b8cb128fa5692729ab3aaa199c1753f726", + UpgradableUniversalResolverProxy: "0x202cce504e04bed6fc0521238ddf04bc9e8e15ab", + + // -- Infrastructure -- + BatchGatewayProvider: "0xdc64a140aa3e981100a9beca4e685f962f0cf6c9", + HCAFactory: "0x0165878a594ca255338adfa4d48449f69242eb8f", + SimpleRegistryMetadata: "0xa513e6e4b8f2a923d98304ec87f64353c4d5c853", + Root: "0x610178da211fef7d417bc0e6fed39f05609ad788", + RootSecurityController: "0xb7f8bc63bbcad18155201308c8f3540b07f84f5e", + RegistrarSecurityController: "0x0b306bf915c4d645ff596e518faf3f9669b97016", + VerifiableFactory: "0x4ed7c70f96b99c776995fb64377f0d4ab3b0e1c1", + NameWrapper: "0x5081a39b8a5f0e35a8d959395a630b68b74dd30f", + UnlockedMigrationController: "0xdbc43ba45381e02825b14322cddd15ec4b3164e6", + WrapperRegistry: "0xd8a5a9b31c3c0232e196d518e89fd8bf83acad43", + LockedMigrationController: "0x36b58f5c1969b7b6591d752ea6f5486d069010ab", + UserRegistryImpl: "0x7969c5ed335650692bc04293b07f5bf2e7a673c0", + StaticMetadataService: "0xb0d4afd8879ed9f52b28595d31b441d079b2ca07", + Multicall3: "0xca11bde05977b3631167028862be2a173976ca11", + MigrationHelper: "0x5c74c94173f05da1720953407cbb920f3df9f887", + + // -- DNSSEC Algorithms & Digests -- + RSASHA1Algorithm: "0xa85233c63b9ee964add6f2cffe00fd84eb32338f", + RSASHA256Algorithm: "0x4a679253410272dd5232b3ff7cf5dbb88f295319", + P256SHA256Algorithm: "0x7a2088a1bfc9d81c55368ae168c2c02570cb814f", + SHA1Digest: "0x09635f643e140090a9a8dcd712ed6285858cebef", + SHA256Digest: "0xc5a5c42992decbae36851359345fe25997f5c42d", + DNSSECImpl: "0x67d269191c92caf3cd7723f116c85e6e9bf55933", + + // -- Pricing -- + StandardRentPriceOracle: "0x1429859428c0abc9c2c47c8ee9fbaf82cfa0f20f", + StaticBulkRenewal: "0x7a9ec1d04904907de0ed7b6839ccdd59c3716ac9", + DummyOracle: "0x2b0d36facd61b71cc05ab8f3d2355ec3631c0dd5", + ExponentialPremiumPriceOracle: "0xfbc22278a96299d91d41c453234d97b4f5eb9b2d", + + // -- Mock Tokens -- + MockUSDC: "0xfd471836031dc5108809d173a067e8486b9047a3", + MockDAI: "0xcbeaf3bde82155f56486fb5a1072cb8baaf547cc", +} as const satisfies Record; + +/** + * Must match the devnet mnemonic in contracts-v2 (Anvil named accounts). + * @see https://github.com/ensdomains/contracts-v2/blob/69bde1b345c47caf3d55a105b9f922280ba55f00/contracts/script/setup.ts#L56 + */ +const mnemonic = "test test test test test test test test test test test junk"; + +function createAccount(addressIndex: number, resolver: NormalizedAddress) { + const account = mnemonicToAccount(mnemonic, { addressIndex }); + return { + ...account, + address: toNormalizedAddress(account.address), + resolver, + }; +} + +/** + * Named accounts from the ens-test-env devnet. + * They are NOT real Ethereum Mainnet or testnet addresses. + * You can use `pnpm devnet` to see actual data in devnet + * + * @see https://github.com/ensdomains/ens-test-env + */ +export const accounts = { + deployer: createAccount(0, asNormalizedAddress("0x1f2ce8886692b90f5754a7d428a2336800a5911b")), + owner: createAccount(1, asNormalizedAddress("0x5ea90acf6555276660760fe629d72932c91f4b8e")), + user: createAccount(2, asNormalizedAddress("0xb63ae54076c1c281ec9395b290add470e69140c6")), + user2: createAccount(3, asNormalizedAddress("0x5380066832977eb36353fd2b01fb92e751636b84")), +} as const; + +/** + * Fixtures for seeding the devnet with test data. + */ +export const addresses = { + one: asNormalizedAddress(`0x${"1".repeat(40)}`), +} 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/ens-test-env.ts index 249cf6dfa0..f864cd921d 100644 --- a/packages/datasources/src/ens-test-env.ts +++ b/packages/datasources/src/ens-test-env.ts @@ -11,6 +11,7 @@ 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 { contracts } from "./devnet/constants"; import { ensTestEnvChain } from "./lib/chains"; // Shared ABIs import { ResolverABI } from "./lib/ResolverABI"; @@ -42,13 +43,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: { @@ -58,41 +59,41 @@ export default { // NOTE: named BaseRegistrarImplementation in devnet BaseRegistrar: { abi: root_BaseRegistrar, - address: "0x8a791620dd6260079bf849dc5567adc3f2fdc318", + address: contracts.BaseRegistrarImplementation, startBlock: 0, }, // NOTE: named LegacyETHRegistrarController in devnet LegacyEthRegistrarController: { abi: root_LegacyEthRegistrarController, - address: "0x46b142dd1e924fab83ecc3c08e4d46e82f005e0e", + 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: "0x367761085bf3c12e5da2df99ac6e1a824612b8fb", + address: contracts.ETHRegistrarController, startBlock: 0, }, NameWrapper: { abi: root_NameWrapper, - address: "0x5081a39b8a5f0e35a8d959395a630b68b74dd30f", + address: contracts.NameWrapper, startBlock: 0, }, UniversalResolver: { abi: UniversalResolverV1, - address: "0xaa292e8611adf267e563f334ee42320ac96d0463", + address: contracts.UniversalResolver, startBlock: 0, }, // NOTE: named UniversalResolverV2 in devnet UniversalResolverV2: { abi: UniversalResolverV2, - address: "0x0355b7b8cb128fa5692729ab3aaa199c1753f726", + address: contracts.UniversalResolverV2, startBlock: 0, }, }, @@ -106,22 +107,22 @@ 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: "0x21df544947ba3e8b3c32561399e88b52dc8b2823", + address: contracts.ETHRegistrar, startBlock: 0, }, ENSv1Resolver: { abi: ResolverABI, - address: "0x5fc8d32690cc91d4c39d9d3abcbd16989f875707", + address: contracts.ENSV1Resolver, startBlock: 0, }, ENSv2Resolver: { @@ -137,28 +138,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: "0xa4899d35897033b927acfcf422bc745916139776", + address: contracts.LegacyPublicResolver, startBlock: 0, }, // NOTE: named PublicResolver in devnet DefaultPublicResolver5: { abi: ResolverABI, - address: "0xf953b3a269d80e3eb0f2947630da976b896a8c5b", + address: contracts.PublicResolver, startBlock: 0, }, }, 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/datasources/src/lib/chains.ts b/packages/datasources/src/lib/chains.ts index 48eac61a4f..7b915502d3 100644 --- a/packages/datasources/src/lib/chains.ts +++ b/packages/datasources/src/lib/chains.ts @@ -1,9 +1,11 @@ import { anvil, type Chain, sepolia } from "viem/chains"; +/** + * NOTE: devnet uses anvil's default chain id of 31337, but we over-specify it here for documentation + * @see https://github.com/ensdomains/contracts-v2/blob/580c60a20e80decce21cf15aafd762f96a96d544/contracts/script/setup.ts#L55 + */ export const ensTestEnvChain = { ...anvil, - // NOTE: devnet uses anvil's default chain id of 31337, but we over-specify it here for documentation - // https://github.com/ensdomains/contracts-v2/blob/580c60a20e80decce21cf15aafd762f96a96d544/contracts/script/setup.ts#L55 id: 31337, name: "ens-test-env", } as const satisfies Chain; 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 01743d2375..15510afee2 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/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 a9e5e4df1d..d4e0127fdb 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 { DevnetAccounts } from "../shared/devnet-accounts"; import type { NamespaceSpecificValue } from "../shared/namespace-specific-value"; const SEPOLIA_V2_V2_ETH_REGISTRY = maybeGetDatasourceContract( @@ -193,7 +193,7 @@ query AccountDomains( }`, variables: { default: { address: VITALIK_ADDRESS }, - [ENSNamespaceIds.EnsTestEnv]: { address: DevnetAccounts.owner.address }, + [ENSNamespaceIds.EnsTestEnv]: { address: accounts.owner.address }, [ENSNamespaceIds.SepoliaV2]: { address: SEPOLIA_V2_USER_ADDRESS }, }, }, @@ -212,7 +212,7 @@ query AccountEvents( }`, variables: { default: { address: VITALIK_ADDRESS }, - [ENSNamespaceIds.EnsTestEnv]: { address: DevnetAccounts.deployer.address }, + [ENSNamespaceIds.EnsTestEnv]: { address: accounts.deployer.address }, [ENSNamespaceIds.SepoliaV2]: { address: SEPOLIA_V2_USER_ADDRESS }, }, }, @@ -296,7 +296,7 @@ query PermissionsByUser($address: Address!) { } }`, variables: { - default: { address: DevnetAccounts.deployer.address }, + default: { address: accounts.deployer.address }, [ENSNamespaceIds.SepoliaV2]: { address: SEPOLIA_V2_USER_ADDRESS }, }, }, @@ -322,7 +322,7 @@ query AccountResolverPermissions($address: Address!) { } }`, variables: { - default: { address: DevnetAccounts.deployer.address }, + default: { address: accounts.deployer.address }, [ENSNamespaceIds.SepoliaV2]: { address: SEPOLIA_V2_USER_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 aaa215d756..0000000000 --- a/packages/ensnode-sdk/src/shared/devnet-accounts.ts +++ /dev/null @@ -1,28 +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 DevnetAccounts = { - deployer: { - address: toNormalizedAddress("0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266"), - resolver: toNormalizedAddress("0x1F2Ce8886692b90F5754a7d428a2336800a5911B"), - }, - owner: { - address: toNormalizedAddress("0x70997970C51812dc3A010C7d01b50e0d17dc79C8"), - resolver: toNormalizedAddress("0x5eA90aCF6555276660760fE629D72932c91f4b8E"), - }, - user: { - address: toNormalizedAddress("0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC"), - resolver: toNormalizedAddress("0xB63aE54076C1c281Ec9395B290aDD470e69140c6"), - }, - user2: { - address: toNormalizedAddress("0x90F79bf6EB2c4f870365E785982E1f101E93b906"), - resolver: toNormalizedAddress("0x5380066832977EB36353fd2B01fb92E751636b84"), - }, -} as const; diff --git a/packages/integration-test-env/src/orchestrator.ts b/packages/integration-test-env/src/orchestrator.ts index a035099b90..182a4cf462 100644 --- a/packages/integration-test-env/src/orchestrator.ts +++ b/packages/integration-test-env/src/orchestrator.ts @@ -6,10 +6,12 @@ * * Phases: * 1. ENSDb (postgres) + devnet via docker-compose (testcontainers DockerComposeEnvironment) - * 2. Start ENSRainbow via `pnpm entrypoint` (downloads + extracts the prebuilt LevelDB in the background) - * 3. Start ENSIndexer, wait for omnichain-following / omnichain-completed - * 4. Start ENSApi - * 5. Run `pnpm test:integration` at the monorepo root + * 2. Seed devnet (primary names and resolver records) + * 3. Start ENSRainbow via `pnpm entrypoint` (downloads + extracts the prebuilt LevelDB in the background) + * 4. Start ENSIndexer + * 5. Wait for omnichain-following / omnichain-completed (indexing complete) + * 6. Start ENSApi + * 7. Run `pnpm test:integration` at the monorepo root * * Design decisions: * - ENSDb (postgres) and devnet are started from docker/docker-compose.orchestrator.yml via @@ -45,6 +47,8 @@ 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"); @@ -61,6 +65,7 @@ const ENSDB_PORT = 5433; const ENSRAINBOW_URL = `http://localhost:${ENSRAINBOW_PORT}`; const ENSINDEXER_SCHEMA_NAME = "ensindexer_integration_test"; const ENSDB_URL = `postgresql://postgres:password@localhost:${ENSDB_PORT}/postgres`; +const RPC_URL = ensTestEnvChain.rpcUrls.default.http[0]; // Track resources for cleanup const subprocesses: ResultPromise[] = []; @@ -265,7 +270,7 @@ async function main() { // Devnet Chain Id check const publicClient = createPublicClient({ - transport: http(ensTestEnvChain.rpcUrls.default.http[0]), + transport: http(RPC_URL), }); const devnetChainId = await publicClient.getChainId(); if (devnetChainId !== ensTestEnvChain.id) { @@ -274,10 +279,14 @@ async function main() { ); } - log("Devnet is ready"); + log(`Devnet is ready (RPC URL: ${RPC_URL})`); + + // Phase 2: Seed devnet with test data (before indexing starts) + log("Seeding devnet..."); + await seedDevnet(RPC_URL); + log("Devnet seeded"); - // Phase 2: Start ENSRainbow via the entrypoint command, which downloads and - // extracts the prebuilt database in the background and serves once attached. + // 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"; @@ -298,7 +307,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 3: Start ENSIndexer + // Phase 4: Start ENSIndexer log("Starting ENSIndexer..."); spawnService( "pnpm", @@ -317,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", @@ -334,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 new file mode 100644 index 0000000000..b8d916bca1 --- /dev/null +++ b/packages/integration-test-env/src/seed/index.ts @@ -0,0 +1,68 @@ +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 new file mode 100644 index 0000000000..ef0fb83c32 --- /dev/null +++ b/packages/integration-test-env/src/seed/primary-names.ts @@ -0,0 +1,20 @@ +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 new file mode 100644 index 0000000000..1fb09c05c7 --- /dev/null +++ b/packages/integration-test-env/src/seed/resolver-records.ts @@ -0,0 +1,163 @@ +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 469f7f3d7e..11f10f6fce 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -851,6 +851,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:*