ChainColor Study
Save and share color grading presets onchain without spending gas or complex wallet setup.
L1 .eth registration· onchain identity
Section · Onchain
full primer →The primitive.
The ENS primitive fires at the right moment in the flow and surfaces a name-shaped, verifiable result that photographers can act on without web3 jargon.
Why this primitiveAn ENS name is the perfect identity anchor for color grading work — ChainColor Study needs a portable, censorship-resistant handle that travels across every wallet, resolver, and dapp in the ecosystem.
Kernel
a commit/reveal registration against the ENSv2 public-beta ETHRegistrar on Sepolia, priced in USD by an on-chain oracle and paid in free mock USDC — no ENS DAO controller approval required
Drives the UI as
a 'mint test USDC, approve, commit, wait 60s, register' flow that ends with the name resolving through Universal Resolver V2
Required keys.
METAMASK_PRIVATE_KEY
Exported from MetaMask. Fund on Sepolia via the Google Cloud faucet before registering a .eth name.
open ↗Add these in your Lovable project under Settings → Secrets before pasting the prompt below.
Appendix · Mega-prompt
The build prompt.
budget · 1 message
Paste into a fresh Lovable project. Make sure all five secrets above are set first. read the build strategy →
Build "ChainColor Study" in ONE Lovable message. Single-page demo where a user
registers a real `.eth` name on the ENSv2 PUBLIC BETA (Sepolia) — permissionless,
no ENS DAO approval, paid in free mock USDC — and then uses that name inside the app.
CONCEPT
Save and share color grading presets onchain without spending gas or complex wallet setup.
Discipline: Photography (color grading).
ENS primitive: ENSv2 permissionless .eth registration on Sepolia (commit/reveal, USD-priced oracle, mock-USDC payment).
Why this primitive: An ENS name is the perfect identity anchor for color grading work — ChainColor Study needs a portable, censorship-resistant handle that travels across every wallet, resolver, and dapp in the ecosystem.
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.
WHY ENSv2 AND NOT V1: the v1 `ETHRegistrarController` is DAO-gated
(`BaseRegistrar.addController`), so a hackathon app cannot register through its own
controller. The ENSv2 `ETHRegistrar` launched as a public beta on Sepolia
(2026-08-13, https://docs.ens.domains/ensv2/overview) and is permissionless.
VERIFIED SEPOLIA ADDRESSES (read live on-chain — do not substitute):
- ETHRegistrar (v2) 0xa88553F454b77203B0D036A05c894d555EAAa2Cc
- Registry (v2) 0xBDC85dD5b15D7ecb354cd7cb6f2c50b4f2c4F0E2
- Mock USDC (6 decimals) 0x768F42455A2D082E23ceeF7d51e5787C82d67a39 (public mint)
- Mock DAI 0x5472C5725A00B7bA11F0794A79D08ade6F4683bD
- StandardRentPriceOracle 0x8914b66260EB8C4fff795650c3AE8Cd335958987
On-chain constants: MIN_COMMITMENT_AGE = 60s, MAX_COMMITMENT_AGE = 86400s.
Price for a normal-length name ~= 8.000021 USDC / year.
ABI — v2 uses FLAT POSITIONAL ARGS, not the v1 `RegistrationArgs` tuple:
isAvailable(string label) view returns (bool)
getRegisterPrice(string label, uint64 duration, address paymentToken)
view returns (uint256 base, uint256 premium)
makeCommitment(string label, address owner, bytes32 secret, address subregistry,
address resolver, uint64 duration, bytes32 referrer) pure returns (bytes32)
commit(bytes32 commitment)
commitmentAt(bytes32 commitment) view returns (uint64)
register(string label, address owner, bytes32 secret, address subregistry,
address resolver, uint64 duration, address paymentToken, bytes32 referrer)
returns (uint256 tokenId)
// Custom errors — MUST be in the ABI or viem decodes reverts as garbage:
error CommitmentTooNew(bytes32 commitment, uint64 validFrom, uint64 blockTimestamp);
error CommitmentTooOld(bytes32 commitment, uint64 validTo, uint64 blockTimestamp);
error UnexpiredCommitmentExists(bytes32 commitment);
error DurationTooShort(uint64 duration, uint64 minDuration);
error NameNotAvailable(string label);
error NotValid(string label);
error InvalidOwner();
error PaymentTokenNotSupported(address paymentToken);
error SafeERC20FailedOperation(address token);
error MaxCommitmentAgeTooLow();
// Mock USDC: decimals() symbol() balanceOf(address) allowance(address,address)
// approve(address,uint256) mint(address,uint256) <- mint is PUBLIC
FLOW (all sponsored via Privy on chainId 11155111):
1. `isAvailable(label)` FIRST. `getRegisterPrice` reverts with selector
0x477707e8 when the name is taken — do not call it blind.
2. duration = 365n*24n*3600n (uint64 SECONDS). price = base + premium, in USDC
6-decimal units (NOT wei, NOT ether).
3. If `usdc.balanceOf(eoa) < price`, call `usdc.mint(eoa, 100_000000n)` (= 100 USDC).
4. If `usdc.allowance(eoa, registrar) < price`, call
`usdc.approve(registrar, price * 2n)`. APPROVE BEFORE COMMIT — a failed
approve after commit wastes the whole 60s wait.
5. secret = 32 random bytes; persist in sessionStorage keyed `ensv2:${label}`.
A page refresh mid-wait makes the commitment unrecoverable.
6. commit(makeCommitment(label, eoa, secret, zeroAddress, zeroAddress, duration, zeroHash)).
subregistry and resolver may be 0x0 for a bare registration.
7. Wait 65s (60s min + chain-time drift) with a live countdown UI.
8. `simulateContract(register, ...)` then send it with the SAME args plus
paymentToken = mock USDC. On CommitmentTooNew, poll `commitmentAt(hash)`
against block.timestamp and auto-retry ONCE.
9. Verify + celebrate via Universal Resolver V2 (block below).
PRE-FLIGHT GUARDS (register button DISABLED until all green):
1. Provider on chainId 11155111 (see Privy TRAP 2).
2. Label matches /^[a-z0-9-]{3,}$/.
3. `isAvailable(label) === true`.
4. USDC balance >= price AND allowance >= price.
5. `simulateContract` succeeds.
BETA CAVEATS (put these in the UI, not just the code):
- ENSv2 names live in the V2 registry — they do NOT appear in the v1
`ENSRegistry.owner(namehash(name))` read. Never use that call to prove ownership.
- Contracts are still evolving during the beta; if an unknown revert selector
appears, re-check the addresses above against https://docs.ens.domains/ensv2/overview
before debugging your own code.
- Phishing warning: ENS will NEVER ask for a seed phrase or private key. Only link
to ens.domains / docs.ens.domains.
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 (index route). No router, no Hardhat, no Solidity.
- viem ^2. src/lib/ensv2.ts: addresses + ABIs above + `price()`, `ensureUsdc()`,
`commitAndRegister()` helpers.
- src/lib/privy-tx.ts: `sendTxFromEoa(embedded, tx)` implementing the four Privy traps.
USER FLOW
1. Land -> "Sign in with Google" (Privy) -> embedded EOA auto-provisioned, shown
with its Sepolia ETH balance and a faucet link.
2. Type a name -> availability + price (in USDC) render live.
3. "Get test USDC" -> "Approve" -> "Commit" -> 60s countdown -> "Register".
Each step is its own sponsored tx with an Etherscan link.
4. Success card shows the name, the tokenId, and the Universal Resolver V2
verification, then the name becomes the user's identity across the rest of the
color grading demo.
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.
- 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
Market sizing.
TAM
$2.4B
color grading and photo editing
SAM
$500M
color grading presets market
SOM
$15M
social sharing of presets onchain
Indicative figures for hackathon pitches — refine with your own research before raising.
Adjacent entries.
photographer profiles
Decentralized Portfolios
Create and showcase decentralized photography portfolios verified on Sepolia smart contracts.
contest managementOnchain Photo Challenges
Host transparent and decentralized photo contests with onchain submission and voting.
legal documentationOnchain Model Releases
Digitally sign and store model release forms on Sepolia blockchain for immutable proof.
color gradingColorChain Palette
Save and share color grading presets and images as permanent IPFS assets for consistent workflow.