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..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,25 +6,27 @@ import { describe, expect, it } from "vitest"; -import { DEVNET_OWNER, DEVNET_USER } 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: DEVNET_OWNER, + 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 }, + 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, + description: "returns null for user without a primary name", + address: accounts.user.address, chainId: "1", query: "", expectedStatus: 200, @@ -32,7 +34,7 @@ describe("GET /api/resolve/primary-name/:address/:chainId", () => { }, { description: "owner address with accelerate=true returns accelerationRequested: true", - address: DEVNET_OWNER, + address: accounts.owner.address, chainId: "1", query: "accelerate=true", expectedStatus: 200, @@ -58,7 +60,7 @@ describe("GET /api/resolve/primary-name/:address/:chainId", () => { }, { description: "returns 400 for non-numeric chainId", - address: DEVNET_OWNER, + 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 0e54e01691..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,31 +6,30 @@ import { describe, expect, it } from "vitest"; -import { DEVNET_OWNER } 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: DEVNET_OWNER, + description: "resolves primary names for owner address on chain 1", + address: accounts.owner.address, 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: accounts.owner.address, query: "", expectedStatus: 200, expectedBody: { - names: { "1": null }, + names: { "1": "test.eth" }, accelerationRequested: false, accelerationAttempted: false, }, @@ -54,7 +53,7 @@ describe("GET /api/resolve/primary-names/:address", () => { }, { description: "returns 400 when chainIds contains the default chain id (0)", - address: DEVNET_OWNER, + address: 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_OWNER, + 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 329696d032..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_OWNER } 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_OWNER } }, + 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_OWNER } }, + 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_OWNER }, + 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_OWNER } }, + 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_OWNER } }, + records: { addresses: { 60: accounts.owner.address } }, accelerationRequested: false, accelerationAttempted: false, }, @@ -124,18 +124,109 @@ describe("GET /api/resolve/records/:name", () => { query: "addresses=60", expectedStatus: 200, expectedBody: { - records: { addresses: { 60: DEVNET_OWNER } }, + 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: DEVNET_OWNER } }, + 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 b3f14f8c94..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_DEPLOYER, DEVNET_OWNER, DEVNET_USER } from "@ensnode/ensnode-sdk/internal"; +import { accounts } from "@ensnode/datasources/devnet"; import { AccountDomainsPaginated, @@ -36,7 +36,9 @@ 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: accounts.owner.address, + }); const domains = flattenConnection(result.account.domains); const names = domains.map((d) => d.name); @@ -62,7 +64,7 @@ describe("Account.domains", () => { it("returns domains owned by the new owner", async () => { const result = await request(AccountDomains, { - address: DEVNET_USER, + address: accounts.user.address, }); const domains = flattenConnection(result.account.domains); const names = domains.map((d) => d.name); @@ -75,7 +77,7 @@ describe("Account.domains pagination", () => { testDomainPagination(async (variables) => { const result = await request<{ account: { domains: PaginatedGraphQLConnection }; - }>(AccountDomainsPaginated, { address: DEVNET_OWNER, ...variables }); + }>(AccountDomainsPaginated, { address: accounts.owner.address, ...variables }); return result.account.domains; }); }); @@ -92,14 +94,14 @@ describe("Account.events", () => { it("returns events for the devnet deployer", async () => { const result = await request(AccountEvents, { - address: DEVNET_DEPLOYER, + 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_DEPLOYER); + expect(event.from).toBe(accounts.deployer.address); } }); }); @@ -108,7 +110,7 @@ describe("Account.events pagination", () => { testEventPagination(async (variables) => { const result = await request<{ account: { events: PaginatedGraphQLConnection }; - }>(AccountEventsPaginated, { address: DEVNET_DEPLOYER, ...variables }); + }>(AccountEventsPaginated, { address: accounts.deployer.address, ...variables }); return result.account.events; }); }); @@ -127,7 +129,7 @@ describe("Account.events filtering (AccountEventsWhereInput)", () => { beforeAll(async () => { const result = await request(AccountEventsFiltered, { - address: DEVNET_DEPLOYER, + address: accounts.deployer.address, first: 1000, }); // events are returned in ascending order, so first/last access yields min/max values @@ -139,7 +141,7 @@ describe("Account.events filtering (AccountEventsWhereInput)", () => { const targetSelector = allEvents[0].topics[0]; const result = await request(AccountEventsFiltered, { - address: DEVNET_DEPLOYER, + address: accounts.deployer.address, where: { selector_in: [targetSelector] }, }); const events = flattenConnection(result.account.events); @@ -152,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_DEPLOYER, + address: accounts.deployer.address, where: { selector_in: ["0x0000000000000000000000000000000000000000000000000000000000000001"], }, @@ -163,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_DEPLOYER, + address: accounts.deployer.address, where: { selector_in: [] }, }); const events = flattenConnection(result.account.events); @@ -174,7 +176,7 @@ describe("Account.events filtering (AccountEventsWhereInput)", () => { const midTimestamp = allEvents[Math.floor(allEvents.length / 2)].timestamp; const result = await request(AccountEventsFiltered, { - address: DEVNET_DEPLOYER, + address: accounts.deployer.address, where: { timestamp_gte: midTimestamp }, }); const events = flattenConnection(result.account.events); @@ -190,7 +192,7 @@ describe("Account.events filtering (AccountEventsWhereInput)", () => { const midTimestamp = allEvents[Math.floor(allEvents.length / 2)].timestamp; const result = await request(AccountEventsFiltered, { - address: DEVNET_DEPLOYER, + address: accounts.deployer.address, where: { timestamp_lte: midTimestamp }, }); const events = flattenConnection(result.account.events); @@ -207,7 +209,7 @@ describe("Account.events filtering (AccountEventsWhereInput)", () => { const maxTs = allEvents[allEvents.length - 1].timestamp; const result = await request(AccountEventsFiltered, { - address: DEVNET_DEPLOYER, + address: accounts.deployer.address, where: { timestamp_gte: minTs, timestamp_lte: maxTs }, first: 1000, }); @@ -224,7 +226,7 @@ describe("Account.events filtering (AccountEventsWhereInput)", () => { const midTimestamp = seedEvent.timestamp; const result = await request(AccountEventsFiltered, { - address: DEVNET_DEPLOYER, + address: accounts.deployer.address, where: { selector_in: [targetSelector], timestamp_gte: midTimestamp }, }); const events = flattenConnection(result.account.events); @@ -241,7 +243,7 @@ describe("Account.events filtering (AccountEventsWhereInput)", () => { const maxTimestamp = BigInt(allEvents[allEvents.length - 1].timestamp); const result = await request(AccountEventsFiltered, { - address: DEVNET_DEPLOYER, + address: accounts.deployer.address, where: { timestamp_gte: (maxTimestamp + 1n).toString() }, }); const events = flattenConnection(result.account.events); 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/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: 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/lib/chains.ts b/packages/datasources/src/lib/chains.ts index 851dfff825..cb4e5c2cc7 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 - * @see https://github.com/ensdomains/contracts-v2/blob/762de44d60b2588b2e92a6d29df941c4de821ae6/contracts/script/setup.ts#L40 + * The ens-test-env chain id is 1: + * @see https://github.com/ensdomains/contracts-v2/blob/9f26a8f01f1f87db1c5d57b9faa8e76f0c5043ef/contracts/script/setup.ts#L91 */ -const DEVNET_DEFAULT_CHAIN_ID = 0xeeeeed; 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/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 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 6f49dc01d6..73baa5d74a 100644 --- a/packages/ensnode-sdk/src/omnigraph-api/example-queries.ts +++ b/packages/ensnode-sdk/src/omnigraph-api/example-queries.ts @@ -1,6 +1,7 @@ import { asInterpretedName, toNormalizedAddress } from "enssdk"; import { DatasourceNames, ENSNamespaceIds } from "@ensnode/datasources"; +import { accounts } from "@ensnode/datasources/devnet"; import { maybeGetDatasourceContract } from "../shared/datasource-contract"; import type { NamespaceSpecificValue } from "../shared/namespace-specific-value"; @@ -29,12 +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"); const DEVNET_NAME_WITH_OWNED_RESOLVER = asInterpretedName("example.eth"); @@ -186,7 +181,7 @@ query AccountDomains( }`, variables: { default: { address: VITALIK_ADDRESS }, - [ENSNamespaceIds.EnsTestEnv]: { address: DEVNET_OWNER }, + [ENSNamespaceIds.EnsTestEnv]: { address: accounts.owner.address }, }, }, @@ -204,7 +199,7 @@ query AccountEvents( }`, variables: { default: { address: VITALIK_ADDRESS }, - [ENSNamespaceIds.EnsTestEnv]: { address: DEVNET_DEPLOYER }, + [ENSNamespaceIds.EnsTestEnv]: { address: accounts.deployer.address }, }, }, @@ -287,7 +282,7 @@ query PermissionsByUser($address: Address!) { } }`, variables: { - default: { address: DEVNET_DEPLOYER }, + default: { address: accounts.deployer.address }, // TODO: figure out a good sepolia-v2 user address // [ENSNamespaceIds.SepoliaV2]: { address: "" }, }, @@ -314,7 +309,7 @@ query AccountResolverPermissions($address: Address!) { } }`, variables: { - default: { address: DEVNET_DEPLOYER }, + 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/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/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/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 17e2a72d58..79a1965caa 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 DOCKER_DIR = resolve(MONOREPO_ROOT, "docker"); const ENSRAINBOW_DIR = resolve(MONOREPO_ROOT, "apps/ensrainbow"); @@ -252,9 +254,17 @@ 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 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"; @@ -301,7 +311,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", @@ -320,10 +330,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", @@ -337,7 +347,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/abi.ts b/packages/integration-test-env/src/seed/abi.ts new file mode 100644 index 0000000000..16e8ef3afa --- /dev/null +++ b/packages/integration-test-env/src/seed/abi.ts @@ -0,0 +1,23 @@ +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/index.ts b/packages/integration-test-env/src/seed/index.ts new file mode 100644 index 0000000000..3ef4c7be12 --- /dev/null +++ b/packages/integration-test-env/src/seed/index.ts @@ -0,0 +1,51 @@ +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 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..67f181ae57 --- /dev/null +++ b/packages/integration-test-env/src/seed/primary-names.ts @@ -0,0 +1,19 @@ +import { contracts } from "@ensnode/datasources/devnet"; + +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: 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 new file mode 100644 index 0000000000..295af8e702 --- /dev/null +++ b/packages/integration-test-env/src/seed/resolver-records.ts @@ -0,0 +1,162 @@ +import { type Address, type Hex, namehash, toHex } from "viem"; +import { packetToBytes } from "viem/ens"; + +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", 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: universalResolverV2Abi, + 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: publicResolverAbi, + functionName: "setText", + args: [node, key, value], + }); + await walletClient.waitForTransactionReceipt({ 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: publicResolverAbi, + functionName: "setAddr", + args: [node, coinType, addressBytes], + }); + await walletClient.waitForTransactionReceipt({ 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: publicResolverAbi, + functionName: "setContenthash", + args: [node, hashValue], + }); + await walletClient.waitForTransactionReceipt({ 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: publicResolverAbi, + functionName: "setPubkey", + args: [node, x, y], + }); + await walletClient.waitForTransactionReceipt({ 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: publicResolverAbi, + functionName: "setABI", + args: [node, contentType, data], + }); + await walletClient.waitForTransactionReceipt({ 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: publicResolverAbi, + functionName: "setInterface", + args: [node, interfaceId, implementer], + }); + await walletClient.waitForTransactionReceipt({ hash }); + console.log(`[seed] setInterface(interfaceId=${interfaceId}) 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: