From c686dd1ceecebd9bf3ebfc619bd201ce02e3487f Mon Sep 17 00:00:00 2001 From: Vickler Charles Date: Thu, 8 Jan 2026 14:15:43 +0100 Subject: [PATCH 1/2] feat: add direct Base L2 support for Basenames (*.base.eth) This adds native support for resolving contenthash records from Basenames (*.base.eth) by querying the Base L2 resolver directly, bypassing the Coinbase CCIP gateway which doesn't return contenthash. Changes: - Add BasenamesService to query Base L2 resolver at 0xC6d566A56A1aFf6508b41f6c90ff131615583BCD - Add IConfigurationBase interface for Base RPC configuration - Update NameServiceFactory to route *.base.eth to BasenamesService - Add BASE_RPC_ENDPOINT environment variable (defaults to https://mainnet.base.org) --- .../src/nameservice/BasenamesService.ts | 97 +++++++++++++++++++ .../src/nameservice/index.ts | 10 ++ .../src/configuration/index.ts | 24 +++++ .../src/dependencies/services.ts | 10 ++ packages/dweb-api-types/src/config.ts | 6 ++ 5 files changed, 147 insertions(+) create mode 100644 packages/dweb-api-resolver/src/nameservice/BasenamesService.ts diff --git a/packages/dweb-api-resolver/src/nameservice/BasenamesService.ts b/packages/dweb-api-resolver/src/nameservice/BasenamesService.ts new file mode 100644 index 0000000..30ad8b4 --- /dev/null +++ b/packages/dweb-api-resolver/src/nameservice/BasenamesService.ts @@ -0,0 +1,97 @@ +import { JsonRpcProvider } from "ethers"; +import { ILoggerService } from "dweb-api-types/dist/logger"; +import { IRequestContext } from "dweb-api-types/dist/request-context"; +import { INameService } from "dweb-api-types/dist/name-service"; +import { IConfigurationBase } from "dweb-api-types/dist/config"; +import { getContentHashFallback } from "./utils.js"; +import { namehash } from "ethers"; + +const L2_RESOLVER_ADDRESS = "0xC6d566A56A1aFf6508b41f6c90ff131615583BCD"; +const L2_RESOLVER_ABI = [ + "function contenthash(bytes32 node) view returns (bytes)", +]; + +export class BasenamesService implements INameService { + _configurationService: IConfigurationBase; + provider: JsonRpcProvider; + _logger: ILoggerService; + + constructor( + configurationService: IConfigurationBase, + logger: ILoggerService, + ) { + this._configurationService = configurationService; + const baseConfig = this._configurationService.getConfigBaseBackend(); + const rpc = baseConfig.getBackend(); + + this.provider = new JsonRpcProvider(rpc, undefined, { + staticNetwork: true, + }); + this._logger = logger; + } + + async getContentHash( + request: IRequestContext, + name: string, + ): Promise { + this._logger.debug("BasenamesService: resolving contenthash", { + ...request, + origin: "BasenamesService", + context: { name }, + }); + + try { + const node = namehash(name); + + this._logger.debug("BasenamesService: querying L2 resolver", { + ...request, + origin: "BasenamesService", + context: { name, node, resolver: L2_RESOLVER_ADDRESS }, + }); + + const { Contract } = await import("ethers"); + const contract = new Contract( + L2_RESOLVER_ADDRESS, + L2_RESOLVER_ABI, + this.provider, + ); + + const contenthashBytes = await contract.contenthash(node); + + if (!contenthashBytes || contenthashBytes === "0x") { + this._logger.debug("BasenamesService: no contenthash set", { + ...request, + origin: "BasenamesService", + context: { name }, + }); + return null; + } + + const decoded = getContentHashFallback( + request, + this._logger, + contenthashBytes, + name, + "BasenamesService", + ); + + this._logger.debug("BasenamesService: contenthash resolved", { + ...request, + origin: "BasenamesService", + context: { name, contenthash: decoded }, + }); + + return decoded; + } catch (error: any) { + this._logger.error("BasenamesService: error resolving contenthash", { + ...request, + origin: "BasenamesService", + context: { + name, + error: error.message || error, + }, + }); + return null; + } + } +} diff --git a/packages/dweb-api-resolver/src/nameservice/index.ts b/packages/dweb-api-resolver/src/nameservice/index.ts index d803332..5e16ff6 100644 --- a/packages/dweb-api-resolver/src/nameservice/index.ts +++ b/packages/dweb-api-resolver/src/nameservice/index.ts @@ -11,15 +11,18 @@ export class NameServiceFactory implements INameServiceFactory { _logger: ILoggerService; _ensService: INameService; _web3NameSdkService: INameService; + _basenamesService: INameService | null; constructor( logger: ILoggerService, ensService: INameService, web3NameSdkService: INameService, + basenamesService?: INameService, ) { this._logger = logger; this._ensService = ensService; this._web3NameSdkService = web3NameSdkService; + this._basenamesService = basenamesService || null; } getNameServiceForDomain( @@ -33,6 +36,13 @@ export class NameServiceFactory implements INameServiceFactory { }); return this._web3NameSdkService; } + if (domain.endsWith(".base.eth") && this._basenamesService) { + this._logger.debug("Using BasenamesService for domain " + domain, { + ...request, + origin: "NameServiceFactory", + }); + return this._basenamesService; + } this._logger.debug("Using EnsService for domain " + domain, { ...request, origin: "NameServiceFactory", diff --git a/packages/dweb-api-server/src/configuration/index.ts b/packages/dweb-api-server/src/configuration/index.ts index 6aa952c..7a6bc89 100644 --- a/packages/dweb-api-server/src/configuration/index.ts +++ b/packages/dweb-api-server/src/configuration/index.ts @@ -6,6 +6,7 @@ import { IConfigurationEthereum, IConfigurationEthereumFailover, IConfigurationGnosis, + IConfigurationBase, ICacheConfig, IConfigurationIpfs, IConfigurationServerAsk, @@ -37,6 +38,9 @@ const configuration = { gnosis: { rpc: process.env.GNO_RPC_ENDPOINT || "https://rpc.gnosischain.com", }, + base: { + rpc: process.env.BASE_RPC_ENDPOINT || "https://mainnet.base.org", + }, // Storage backends ipfs: { backend: process.env.IPFS_TARGET || "http://localhost:8080", @@ -195,6 +199,10 @@ export class TestConfigurationService implements ServerConfiguration { return this.getServerConfiguration().getConfigGnosisBackend(); }; + getConfigBaseBackend = () => { + return this.getServerConfiguration().getConfigBaseBackend(); + }; + getCacheConfig = () => { return this.getServerConfiguration().getCacheConfig(); }; @@ -362,6 +370,20 @@ export const configurationToIConfigurationGnosis = (config: { }; }; +export const configurationToIConfigurationBase = (config: { + base: { + rpc: string; + }; +}): IConfigurationBase => { + return { + getConfigBaseBackend: () => { + return { + getBackend: () => config.base.rpc, + }; + }, + }; +}; + export const configurationToICacheConfig = (config: { cache: { ttl: number; @@ -547,6 +569,7 @@ export type ServerConfiguration = IConfigurationServerRouter & IConfigurationEthereum & IConfigurationEthereumFailover & IConfigurationGnosis & + IConfigurationBase & ICacheConfig & IConfigurationServerDnsquery & IDomainQueryConfig & @@ -567,6 +590,7 @@ export const configurationToServerConfiguration = ( ...configurationToIConfigurationEthereum(config), ...configurationToIConfigurationEthereumFailover(config), ...configurationToIConfigurationGnosis(config), + ...configurationToIConfigurationBase(config), ...configurationToICacheConfig(config), ...configurationToIDomainQueryConfig(config), ...configurationToIRedisConfig(config), diff --git a/packages/dweb-api-server/src/dependencies/services.ts b/packages/dweb-api-server/src/dependencies/services.ts index f4fb0bb..51bcf45 100644 --- a/packages/dweb-api-server/src/dependencies/services.ts +++ b/packages/dweb-api-server/src/dependencies/services.ts @@ -48,6 +48,7 @@ import { NameServiceFactory } from "dweb-api-resolver/dist/nameservice/index"; import {} from "dweb-api-resolver/dist/resolver/index"; import { Web3NameSdkService } from "dweb-api-resolver/dist/nameservice/Web3NameSdkService"; import { EnsService } from "dweb-api-resolver/dist/nameservice/EnsService"; +import { BasenamesService } from "dweb-api-resolver/dist/nameservice/BasenamesService"; export const createApplicationConfigurationBindingsManager = () => { const configuration = new EnvironmentBinding({ @@ -167,6 +168,12 @@ export const createApplicationConfigurationBindingsManager = () => { [EnvironmentConfiguration.Development]: (_env) => new TestResolverService(), }); + const basenamesService = new EnvironmentBinding({ + [EnvironmentConfiguration.Production]: (env) => + new BasenamesService(configuration.getBinding(env), logger.getBinding(env)), + [EnvironmentConfiguration.Development]: (_env) => new TestResolverService(), + }); + const domainRateLimit = new EnvironmentBinding({ [EnvironmentConfiguration.Production]: (env) => new DomainRateLimitService( @@ -192,12 +199,14 @@ export const createApplicationConfigurationBindingsManager = () => { logger.getBinding(env), ensService.getBinding(env), web3NameSdk.getBinding(env), + basenamesService.getBinding(env), ), [EnvironmentConfiguration.Development]: (env) => new NameServiceFactory( logger.getBinding(env), ensService.getBinding(env), web3NameSdk.getBinding(env), + basenamesService.getBinding(env), ), }); @@ -249,6 +258,7 @@ export const createApplicationConfigurationBindingsManager = () => { kuboApi, web3NameSdk, ensService, + basenamesService, domainRateLimit, arweaveResolver, nameServiceFactory, diff --git a/packages/dweb-api-types/src/config.ts b/packages/dweb-api-types/src/config.ts index 807a13c..0bca5a8 100644 --- a/packages/dweb-api-types/src/config.ts +++ b/packages/dweb-api-types/src/config.ts @@ -45,6 +45,12 @@ export interface IConfigurationGnosis { }; } +export interface IConfigurationBase { + getConfigBaseBackend: () => { + getBackend: () => string; + }; +} + export type IConfigurationLogger = { getLoggerConfig: () => { getLevel: () => "warn" | "error" | "info" | "debug"; From 092e86f77de3bd57cf811632856f9ab197830232 Mon Sep 17 00:00:00 2001 From: Vickler Charles Date: Mon, 9 Feb 2026 16:59:12 +0100 Subject: [PATCH 2/2] Add integration tests and docs for Basenames (*.base.eth) support Add test cases for example.base.eth and norecord.base.eth, wire up testBasenamesService in the integration test harness, and document the BASE_RPC_ENDPOINT environment variable in README. --- README.md | 1 + packages/dweb-api-server/src/test/cases.json | 12 ++++++++++++ .../dweb-api-server/src/test/integration.spec.ts | 9 ++++++++- 3 files changed, 21 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 714c0f4..29306ec 100644 --- a/README.md +++ b/README.md @@ -32,6 +32,7 @@ __Gateway request flow__ | `ETH_RPC_ENDPOINT` | `"http://192.168.1.7:8845"` | Primary RPC provider FQDN for ENS resolution. | | `ETH_RPC_ENDPOINT_FAILOVER_PRIMARY` | `null` | Secondary failover RPC provider FQDN. | | `GNO_RPC_ENDPOINT` | `https://rpc.gnosischain.com` | Primary RPC endpoint for Gnosis. | +| `BASE_RPC_ENDPOINT` | `https://mainnet.base.org` | Primary RPC endpoint for Base L2 (Basenames `*.base.eth` resolution). | | `DOMAINSAPI_ENDPOINT` | `null` | API endpoint for custom domain routing logic. Can be set to any endpoint that returns a `200` if you do not need this feature. | | `LOG_LEVEL` | `"info"` | Set the logging level. | | `LIMO_HOSTNAME_SUBSTITUTION_CONFIG` | `{ "eth.limo": "eth", "eth.local": "eth", "gno.limo": "gno", "gno.local": "gno" }` | The domains and services corresponding to each domain name for gateway operations. When set via an environment variable, this must be a base64 encoded JSON object. | diff --git a/packages/dweb-api-server/src/test/cases.json b/packages/dweb-api-server/src/test/cases.json index cea0484..8279302 100644 --- a/packages/dweb-api-server/src/test/cases.json +++ b/packages/dweb-api-server/src/test/cases.json @@ -82,5 +82,17 @@ "type": "none", "contentHash": null, "additionalInfo": {} + }, + { + "name": "example.base.eth", + "type": "ipfs", + "contentHash": "ipfs://bafkreibyh6otmd37y7edohwbjwydmqpxxygarmrur7j4xwhjejnskw3kta", + "additionalInfo": {} + }, + { + "name": "norecord.base.eth", + "type": "none", + "contentHash": null, + "additionalInfo": {} } ] diff --git a/packages/dweb-api-server/src/test/integration.spec.ts b/packages/dweb-api-server/src/test/integration.spec.ts index 413774b..8e976b8 100644 --- a/packages/dweb-api-server/src/test/integration.spec.ts +++ b/packages/dweb-api-server/src/test/integration.spec.ts @@ -41,6 +41,7 @@ type HarnessType = { hostnameSubstitionService: IHostnameSubstitutionService; testEnsService: TestResolverService; web3NameSdkService: TestResolverService; + testBasenamesService: TestResolverService; testArweaveResolverService: TestResolverService; testDomainQuerySuperagentService: TestDomainQuerySuperagentService; domainQueryService: IDomainQueryService; @@ -71,6 +72,9 @@ let buildAppContainer = (): HarnessType => { web3NameSdkService: services.web3NameSdk.getBinding( EnvironmentConfiguration.Development, ) as TestResolverService, + testBasenamesService: services.basenamesService.getBinding( + EnvironmentConfiguration.Development, + ) as TestResolverService, testArweaveResolverService: services.arweaveResolver.getBinding( EnvironmentConfiguration.Development, ) as TestResolverService, @@ -225,11 +229,14 @@ const harness = const resolvers = [ harnessInput.testEnsService, harnessInput.web3NameSdkService, + harnessInput.testBasenamesService, ]; var theRealTestResolverService: TestResolverService; - if (nameResolvedToEnsName.endsWith("eth")) { + if (nameResolvedToEnsName.endsWith(".base.eth")) { + theRealTestResolverService = harnessInput.testBasenamesService; + } else if (nameResolvedToEnsName.endsWith("eth")) { theRealTestResolverService = harnessInput.testEnsService; } else if (nameResolvedToEnsName.endsWith("gno")) { theRealTestResolverService = harnessInput.web3NameSdkService;