🎬 Filmmaking & Animation · effects asset curation

VFXChain Sync

Pin VFX layers and settings on IPFS for permanent, sharable effects asset libraries.

Durin L2 subname registrar· cheap subnames
Section · Onchain

The primitive.

full primer →

Filmmakers mint a subname on Base Sepolia via Durin for pennies, and the same name resolves back on L1 through CCIP-Read — every effects asset curation entry gets a portable, cheap ENS identity.

Why this primitiveDurin puts subname minting on an L2 so VFXChain Sync can hand out one effects asset curation name per user for pennies, while L1 CCIP-Read keeps the names resolvable from every ENS-aware surface.

Kernel
a Durin L2Registrar deployed to Base Sepolia that mints <name>.<parent>.eth as an L2 NFT, resolved on L1 via a Durin-compatible CCIP-Read resolver
Drives the UI as
a subname minter that fires the tx on Base Sepolia, then verifies the name resolves back on Ethereum Sepolia through viem
Appendix · Secrets

Required keys.

METAMASK_PRIVATE_KEY
Exported from MetaMask. Fund on Sepolia via the Google Cloud faucet before registering a .eth name.
open ↗
SEPOLIA_RPC_URL
Alchemy Sepolia HTTPS endpoint. viem uses this to resolve every .eth name.
open ↗
ETHERSCAN_API_KEY
Required for npx hardhat verify on ENS resolvers / L2 registrars.
open ↗
PRIVY_APP_ID
Google sign-in + sponsored transactions so users never see gas.
open ↗
PINATA_JWT
Pin site bundles / metadata to IPFS, then write the CID as the ENS contenthash.
open ↗

Add these in your Lovable project under Settings → Secrets before pasting the prompt below.

Appendix · Mega-prompt

The build prompt.

Paste into a fresh Lovable project. Make sure all five secrets above are set first. read the build strategy →

Build "VFXChain Sync" in ONE Lovable message. Single-page demo that mints
CHEAP subnames on Base Sepolia via a Durin L2 registrar (namestonehq/durin).
This bypasses ENS DAO approval entirely — anyone can deploy a Durin L2
registrar and issue subnames on L2.

CONCEPT
Pin VFX layers and settings on IPFS for permanent, sharable effects asset libraries.
Discipline: Filmmaking & Animation (effects asset curation).
ENS primitive: Durin L2 subname registrar on Base Sepolia (84532), factory-deployed, L1-wired via CCIP-Read.
Why this primitive: Durin puts subname minting on an L2 so VFXChain Sync can hand out one effects asset curation name per user for pennies, while L1 CCIP-Read keeps the names resolvable from every ENS-aware surface.

5-CREDIT BUDGET (HARD LIMIT):
- ONE single-page app. No router, no Lovable Cloud, no database, no auth
  flows beyond Privy drop-in.
- No custom Solidity unless the hook explicitly deploys one — reuse the
  canonical ENS / Durin / Namestone contracts listed below.
- Privy is always the auth + sponsored-tx layer (Google login, embedded
  wallet, native gas sponsorship on Sepolia and Base Sepolia).
- At most ONE AI call per user action (Lovable AI Gateway + LOVABLE_API_KEY
  if AI is part of the idea).
- Skip tests, skip CI, skip docs pages. Ship the demo, nothing else.

ENS SPECIFICS — Durin on Base Sepolia:

Canonical Durin factory (Base Sepolia 84532):
  L2RegistryFactory  0xDddddDdDDD8Aa1f237b4fa0669cb46892346d22d

Deploy the L2 registry ONCE (via a small Hardhat script or a one-shot admin
route protected by BASE_SEPOLIA_PRIVATE_KEY — never expose it client-side):
  factory.deploy(baseName, symbol, admin) -> L2Registry address
Then call `L2Registry.setRegistrar(deployer, true)` so your app can mint.
Save the resulting registry address to `src/data/durin.json`.

L2 mint (from the browser, sponsored by Privy on Base Sepolia):
  L2Registry.register(label, owner)
  // label = "alice" -> alice.<parent>.eth resolvable via Durin L1 resolver.

L1 wiring (OPTIONAL for the hackathon — only works if the parent `.eth`
name is registered on Sepolia). Guard the wiring step with
`ENSRegistry.owner(namehash(parent)) !== 0x0`.
  parent.resolver = Durin L1 resolver (per docs, chain-specific)
  durinL1Resolver.setL2Registry(namehash(parent), 84532, l2RegistryAddress)
If the parent isn't registered, ship the demo behind a clear banner:
  "L2-ONLY PREVIEW: subnames are minted on Base Sepolia but won't resolve
   from L1 until <parent>.eth is registered on Sepolia and pointed at the
   Durin L1 resolver."
DO NOT attempt setResolver on an unregistered parent — it reverts with no
useful reason.

Privy chain-switching for Base Sepolia (same four traps as Sepolia, but
chainId 84532 / '0x14a34'):
  await embedded.switchChain(84532);
  const p = await embedded.getEthereumProvider();
  if ((await p.request({method:'eth_chainId'})) !== '0x14a34')
    throw new Error('Provider still not on Base Sepolia — re-open Privy');

Pre-flight guards:
1. Registry address in `src/data/durin.json` is non-empty AND deployer is
   a registered registrar: `L2Registry.registrars(deployer) === true`.
2. Provider on 84532 (see above).
3. Label matches `/^[a-z0-9-]{1,63}$/` and is not already minted
   (`L2Registry.available(labelhash) === true`).

Hardhat (only for the one-time L2 deploy — keep OUTSIDE the Vite bundle,
in /contracts):
  npm i -D @nomicfoundation/hardhat-toolbox @nomicfoundation/hardhat-verify@latest
  // hardhat.config.cjs — Etherscan v2 single-key shape + Basescan customChain:
  require("@nomicfoundation/hardhat-toolbox");
  require("@nomicfoundation/hardhat-verify");
  module.exports = {
    solidity: { version: "0.8.24", settings: { optimizer: { enabled: true, runs: 200 } } },
    networks: {
      baseSepolia: {
        url: process.env.BASE_SEPOLIA_RPC_URL,
        accounts: [process.env.BASE_SEPOLIA_PRIVATE_KEY.startsWith("0x")
          ? process.env.BASE_SEPOLIA_PRIVATE_KEY
          : "0x" + process.env.BASE_SEPOLIA_PRIVATE_KEY],
        chainId: 84532,
      },
    },
    // Etherscan v2 uses ONE key across all chains — NOT the per-network map:
    etherscan: {
      apiKey: process.env.BASESCAN_API_KEY,
      customChains: [{ network: "baseSepolia", chainId: 84532,
        urls: { apiURL: "https://api-sepolia.basescan.org/api",
                 browserURL: "https://sepolia.basescan.org" } }],
    },
    sourcify: { enabled: false },
  };
Deploy: `npx hardhat run scripts/deployDurin.cjs --network baseSepolia`
Verify: `npx hardhat verify --network baseSepolia <registry>`
On success Basescan says "Successfully verified contract" and the source
becomes readable at https://sepolia.basescan.org/address/<addr>#code.

UNIVERSAL RESOLVER V2 (use this for EVERY name read — it is the only
reader that works across v1, ENSv2, offchain ENSIP-25 and Durin L2 names):
- Sepolia address: 0xeEeEEEeE14D718C2B47D9923Deab1335E144EeEe
- ABI subset:
    resolve(bytes name, bytes data) view returns (bytes result, address resolver)
    findCanonicalName(bytes name) view returns (bytes canonicalName, address resolver)
    reverse(bytes lookupAddress, uint256 coinType) view returns (string name, address resolver, address reverseResolver)
- `name` is DNS-WIRE-ENCODED, not a string and not a namehash:
    import { toHex, namehash } from 'viem';
    import { packetToBytes } from 'viem/ens';
    const wire = toHex(packetToBytes('alice.eth'));
- Always read through a viem PUBLIC CLIENT (CCIP-Read is on by default). Reading
  through Privy's embedded provider throws `OffchainLookup` and breaks offchain
  and L2 names.
- `ResolverNotFound` means "no resolver set", NOT "name unregistered" — show
  different copy for each.
- Reverse records are opt-in; most Sepolia addresses have none. Fall back to the
  truncated address, never "unknown".
- After every register/mint, verify with a Universal Resolver read and show the
  resolved address in the success card. It is a free call.

PRIVY (SSR-safe mount + embedded-EOA routing — the four traps):
- Never import @privy-io/react-auth at module scope of a route file. Use
    const Client = lazy(() => import('./privy-client-entry'));
  inside <ClientOnly><Suspense>...</Suspense></ClientOnly>. Put
  <PrivyProvider> only inside privy-client-entry.tsx.
- PrivyProvider config (do NOT stub defaultChain as { id, name } — pass
  viem's `sepolia` or omit; chainId is passed per-call):
    <PrivyProvider appId={import.meta.env.VITE_PRIVY_APP_ID}
      config={{ loginMethods:['google','email'],
                embeddedWallets:{ ethereum:{ createOnLogin:'users-without-wallets' } },
                appearance:{ theme:'dark' } }}>
- TRAP 1 — smart account vs EOA. `wallets[0]` and `useSendTransaction`
  can pick an EIP-4337 smart account that is EMPTY even when your embedded
  EOA is funded. Always pick the embedded EOA explicitly:
    const embedded = wallets.find(w => w.walletClientType === 'privy');
    const provider = await embedded.getEthereumProvider();
  Display `embedded.address` (not smartAccount.address) as the fund-me box
  and poll THAT balance.
- TRAP 2 — stale RPC after switchChain. Privy caches the provider's RPC
  URL. After every switchChain, re-fetch AND assert:
    await embedded.switchChain(11155111);
    const p = await embedded.getEthereumProvider();
    if ((await p.request({method:'eth_chainId'})) !== '0xaa36a7')
      throw new Error('Provider still not on Sepolia — re-open Privy');
- TRAP 3 — `signal is aborted without reason`. Do NOT pass
  uiOptions:{ showWalletUIs:false } (or any uiOptions) on the embedded
  path. Send with ONLY { to, data, value, chainId }.
- TRAP 4 — sponsored tx hangs. Wrap every send in a 45s Promise.race
  whose reject message names the exact dashboard toggle:
    await Promise.race([
      sendTransaction({ to, data, value, chainId: 11155111 },
                     { address: embedded.address, sponsor: true }),
      new Promise((_, r) => setTimeout(() => r(new Error(
        "Privy sendTransaction timed out after 45s. In Privy dashboard: Gas sponsorship -> App pays -> add Ethereum Sepolia (and Base Sepolia if used) -> Allow transactions from the client."
      )), 45_000)),
    ]);
- Do NOT add ZeroDev / SmartWalletsProvider / paymaster URL. Native
  Privy sponsorship works with only the dashboard toggle above.
- DASHBOARD PREREQUISITE (one-time): Privy dashboard -> Gas sponsorship
  -> App pays -> add "Ethereum Sepolia" (and "Base Sepolia" if the hook
  uses it) -> toggle "Allow transactions from the client" ON.

STACK
- React + Vite single page. No router.
- viem public clients for BOTH sepolia and baseSepolia.
- src/data/durin.json — { registry, baseName, chainId: 84532 } after deploy.
- src/lib/durin.ts — mint + availability + L1 wiring status.

USER FLOW
1. Land -> "Sign in with Google" (Privy) -> embedded EOA on Base Sepolia.
2. Pre-flight badge shows registry deployed + provider on 84532.
3. If L1 parent unregistered, banner explains L2-only mode.
4. Enter a label -> "Claim subname on Base Sepolia" -> sponsored tx ->
   success card with Basescan link + "your L2 name" pill used throughout
   the effects asset curation workflow.
5. Footer renders the credit line below.

REQUIRED SECRETS (Lovable -> Project Settings -> Secrets):
- SEPOLIA_RPC_URL (+ VITE_SEPOLIA_RPC_URL, same value) Alchemy Sepolia HTTPS endpoint. Create a free app at https://dashboard.alchemy.com/. Public RPCs throttle/fail — Alchemy required.
- BASE_SEPOLIA_RPC_URL (+ VITE_BASE_SEPOLIA_RPC_URL) Alchemy Base Sepolia HTTPS URL.
- METAMASK_PRIVATE_KEY  EOA that will deploy / own the parent name. Fund it via https://cloud.google.com/application/web3/faucet/ethereum/sepolia
- BASE_SEPOLIA_PRIVATE_KEY  EOA funded on Base Sepolia (same key OK if bridged). Faucet: https://www.alchemy.com/faucets/base-sepolia
- ETHERSCAN_API_KEY  Etherscan v2 verify. Get: https://etherscan.io/myapikey
- BASESCAN_API_KEY  Basescan v2 verify. Get: https://basescan.org/myapikey
- PRIVY_APP_ID (+ VITE_PRIVY_APP_ID) Google sign-in + sponsored tx. Docs: https://docs.privy.io/llms-full.txt

CREDIT (must appear in UI footer):
Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14
Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14
Appendix · Market

Market sizing.

TAM
$1B
VFX production tools
SAM
$300M
VFX asset marketplaces
SOM
$45M
mid-size studios

Indicative figures for hackathon pitches — refine with your own research before raising.

See also

Adjacent entries.