Skip to content

Refactoring domain resolving out of this repo #68

@Rinse12

Description

@Rinse12

Chain Provider Configuration Architecture (v2)

Problem

The current single chainProviders config doesn't support specialized RPCs (e.g., ethrpc.xyz only works for ENS, blocks other contracts). Need a design that:

  • Removes global chainProviders entirely
  • Makes domain resolvers the primary source of RPC URLs
  • Allows challenges to fall back to resolver URLs or use their own
  • Is fully JSON-serializable for user account storage

Design Principles

  1. No global chainProviders - remove from PlebbitOptions
  2. Resolvers own their URLs - each resolver (ENS, SNS) brings its own RPC config
  3. Challenges have fallback chain - challenge options > resolver URLs > challenge hardcoded defaults
  4. chainId hardcoded - resolvers/challenges know their chainId internally (eth=1, matic=137, etc.)

Domain Resolver Architecture

Static Registry: PKC.domainResolvers

Similar to existing Plebbit.challenges, domain resolvers are registered in a static object:

// Static registry (like Plebbit.challenges)
PKC.domainResolvers: { [name: string]: DomainResolverInstance }

// Instance interface (registered directly, no cloning needed)
export interface DomainResolverInstance {
  name: string;
  tlds: string[];           // e.g., ['.eth']
  // chainProviders passed as arg to resolve(), not stored on instance
  resolve: (args: {
    domain: string;
    txtRecordName: string;
    chainProviders: ChainProviders;  // user's config passed at resolve time
    plebbit: Plebbit;
  }) => Promise<DomainResolverResult>;
}

// ChainProviders type (same as current)
type ChainProviders = {
  [chainTicker: string]: {
    urls: string[];
    chainId: number;
  }
};

Registration (App Startup)

import PKC from '@pkc/pkc-js';
import { ensResolver } from '@bitsocial/resolver-ens';
import { snsResolver } from '@bitsocial/resolver-sns';

// Register resolver instances directly (once at app startup)
PKC.domainResolvers['ens'] = ensResolver;
PKC.domainResolvers['sns'] = snsResolver;

// Example ensResolver structure:
// {
//   name: 'ens',
//   tlds: ['.eth'],
//   resolve: async ({ domain, txtRecordName, chainProviders, plebbit }) => {
//     // chainProviders.eth.urls contains the RPC URLs to use
//     // chainProviders.eth.chainId contains the chain ID
//   }
// }

JSON Config (User Account Storage)

{
  "domainResolvers": [
    {
      "name": "ens",
      "chainProviders": {
        "eth": { "urls": ["https://custom-rpc.com"], "chainId": 1 }
      }
    },
    {
      "name": "sns",
      "chainProviders": {
        "sol": { "urls": ["https://solana-rpc.com"], "chainId": -1 }
      }
    }
  ]
}

Initialization Flow

// Load user config from storage
const userConfig = JSON.parse(savedUserAccount);

// Create PKC instance
const plebbit = await PKC({
  domainResolvers: userConfig.domainResolvers  // [{ name, chainProviders }]
});

// Internally:
// 1. Store configs as-is in plebbit.domainResolvers (no cloning needed)
// 2. At resolve time, lookup PKC.domainResolvers[name] and pass chainProviders to resolve()

Resolution Flow (at resolve time)

// When resolving a domain like "mysite.eth":
// 1. Find config where TLD matches (find resolver for '.eth')
const config = plebbit.domainResolvers.find(c => {
  const resolver = PKC.domainResolvers[c.name];
  return resolver?.tlds.some(tld => domain.endsWith(tld));
});

// 2. Get resolver from static registry
const resolver = PKC.domainResolvers[config.name];

// 3. Call resolve() with chainProviders from user config
const result = await resolver.resolve({
  domain,
  txtRecordName,
  chainProviders: config.chainProviders,  // user's config passed here
  plebbit
});

Priority Resolution Order

For Domain Resolvers:
- Uses its own urls[] directly (configured when resolver is instantiated)

For Challenges/Tipping/etc:
1. User-specified URLs in challenge options (e.g., ethRpcUrl)
2. URLs from registered domainResolvers matching the chainTicker
3. Challenge's own hardcoded fallback URLs

Challenge RPC Resolution

Challenges embed their own helper logic inline (no separate util file needed):

// Inside evm-contract-call challenge
function getChainUrls(
    plebbit: Plebbit,
    chainTicker: string,
    userSpecifiedUrls?: string[],
    hardcodedFallbackUrls?: string[]
): string[] {
    // 1. User-specified URLs from challenge options (highest priority)
    if (userSpecifiedUrls?.length) return userSpecifiedUrls;

    // 2. URLs from resolver configs (merge all that have this chainTicker)
    const mergedUrls: string[] = [];
    for (const config of plebbit.domainResolvers || []) {
        const chainProvider = config.chainProviders?.[chainTicker];
        if (chainProvider?.urls?.length) {
            mergedUrls.push(...chainProvider.urls);
        }
    }
    if (mergedUrls.length) return mergedUrls;

    // 3. Challenge's hardcoded fallback URLs
    if (hardcodedFallbackUrls?.length) return hardcodedFallbackUrls;

    throw new Error(`No RPC URLs found for chain ${chainTicker}`);
}

Example Configurations

Simple (ENS only, using resolver defaults)

// App startup: register resolver
PKC.domainResolvers['ens'] = ensResolver;

// User just enables ENS without custom URLs
const plebbit = await PKC({
    domainResolvers: [{ name: 'ens' }]  // uses resolver's default chainProviders
});

Custom RPC URLs

const plebbit = await PKC({
    domainResolvers: [{
        name: 'ens',
        chainProviders: {
            eth: { urls: ['https://my-custom-rpc.com'], chainId: 1 }
        }
    }]
});

With Challenge-Specific Override

// Subplebbit challenge settings
{
    name: "evm-contract-call",
    options: {
        chainTicker: "eth",
        address: "0x...",
        abi: "...",
        condition: ">1000",
        rpcUrls: "https://eth-mainnet.infura.io/v3/..."  // overrides resolver URLs
    }
}

No Resolvers (Challenge-Only)

const plebbit = await PKC({
    // No domainResolvers - can't resolve .eth/.sol addresses
});

// Challenges still work using their hardcoded fallback URLs
// e.g., evm-contract-call has built-in Infura/Alchemy URLs

Multiple Resolvers with Same Chain (URLs merged for challenges)

PKC.domainResolvers['ens'] = ensResolver;      // has eth chainProvider
PKC.domainResolvers['unstoppable'] = udResolver; // also has eth chainProvider

const plebbit = await PKC({
    domainResolvers: [{ name: 'ens' }, { name: 'unstoppable' }]
});
// For challenges: ETH URLs from both resolvers are merged

JSON-Serializable Config (for user account storage)

{
    "domainResolvers": [
        {
            "name": "ens",
            "chainProviders": {
                "eth": { "urls": ["https://ethrpc.xyz"], "chainId": 1 }
            }
        },
        {
            "name": "sns",
            "chainProviders": {
                "sol": { "urls": ["https://solana-rpc.com"], "chainId": -1 }
            }
        }
    ]
}

Note: The actual resolver implementation is loaded by name from PKC.domainResolvers,
chainProviders override the resolver's defaults.

Files to Modify

  1. src/schema.ts - Remove chainProviders, add domainResolvers config schema
  2. src/types.ts - Add DomainResolverInstance, DomainResolverConfig interfaces
  3. src/plebbit/plebbit.ts - Add static PKC.domainResolvers registry, store plebbit.domainResolvers config
  4. src/domain-resolver.ts - Refactor to use plugin system
  5. src/clients/base-client-manager.ts - Update resolution to use registered resolvers
  6. src/runtime/node/subplebbit/challenges/plebbit-js-challenges/evm-contract-call/index.ts - Add rpcUrls option, embed helper inline

Implementation Steps

Phase 1: Define New Types & Schema

  • Add DomainResolverInstance interface to src/types.ts (name, tlds, resolve)
  • Add DomainResolverConfig type to src/types.ts ({ name: string, chainProviders?: ChainProviders })
  • Add DomainResolverConfigSchema to src/schema.ts (validates { name, chainProviders? })
  • Add optional domainResolvers: DomainResolverConfig[] to PlebbitUserOptionsSchema
  • Remove chainProviders from PlebbitUserOptionsSchema (breaking change)
  • Remove defaultChainProviders constant

Phase 2: Static Registry

  • Add static PKC.domainResolvers: { [name: string]: DomainResolverInstance } object
  • Initialize as empty object {}
  • Export from src/index.ts

Phase 3: Update Plebbit Class

  • Add plebbit.domainResolvers: DomainResolverConfig[] property (stores user configs as-is)
  • In constructor: validate each config name exists in PKC.domainResolvers
  • Throw if resolver not found for config name
  • Remove chainProviders property
  • No cloning needed - configs stored directly

Phase 4: Refactor Domain Resolution

  • Update base-client-manager.ts to find resolver config by TLD
  • Look up resolver in PKC.domainResolvers by config.name
  • Call resolver.resolve() with chainProviders from config
  • Throw ERR_NO_RESOLVER_FOR_TLD if no matching resolver

Phase 5: Update Challenges

  • Add rpcUrls option to evm-contract-call optionInputs
  • Embed helper function inline in challenge
  • Add hardcoded fallback URLs in challenge (e.g., public Infura endpoints)

Phase 6: Tests & Migration

  • Add tests for resolver registration
  • Add tests for JSON config -> instance creation with URL override
  • Test challenge fallback chain
  • Document breaking change in CHANGELOG

Breaking Changes

  • chainProviders removed from PlebbitOptions
  • Must register resolvers via PKC.domainResolvers['name'] = resolverInstance before use
  • JSON config stores { name, chainProviders? }[] - resolvers looked up by name, chainProviders override defaults
  • Challenges work without resolvers (use hardcoded fallbacks)
  • For challenges: URLs from all resolvers with matching chainTicker are merged

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions