Fabric Fusion
Pin multimedia fabric experiments combining textiles and digital art.
The primitive.
Fashion designers claim a real .eth name on Sepolia in one commit-and-reveal flow; every mixed media textile records action after that is signed by an ENS identity that travels across every wallet and dapp.
Why this primitiveAn ENS name is the perfect identity anchor for mixed media textile records work — Fabric Fusion needs a portable, censorship-resistant handle that travels across every wallet, resolver, and dapp in the ecosystem.
Required keys.
Add these in your Lovable project under Settings → Secrets before pasting the prompt below.
The build prompt.
Paste into a fresh Lovable project. Make sure all five secrets above are set first. read the build strategy →
Build "Fabric Fusion" in ONE Lovable message. Single-page demo that lets a
user register a `.eth` name on Sepolia end-to-end and use it inside the app.
CONCEPT
Pin multimedia fabric experiments combining textiles and digital art.
Discipline: Fashion & Textile Design (mixed media textile records).
ENS primitive: L1 .eth registration on Sepolia via the canonical ETHRegistrarController (commit/reveal, tuple RegistrationArgs ABI).
Why this primitive: An ENS name is the perfect identity anchor for mixed media textile records work — Fabric Fusion 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.
ENS SPECIFICS — Sepolia L1 register (get any of this wrong and register() reverts):
Canonical Sepolia contracts (as of 2026):
- ETHRegistrarController 0xfb3cE5D01e0f33f41DbB39035dB9745962F1f968 (DAO-approved, tuple ABI)
- PublicResolver 0xE99638b40E4Fff0129D56f03b55b6bbC4BBE49b5
- ENS Registry 0x00000000000C2E074eC69A0dFb2997BA6C7d2e1e
- BaseRegistrar 0x0635513f179D50A207757E05759CbD106d7dFcE8
- ReverseRegistrar 0xA0a1AbcDAe1a2a4A2EF8e9113Ff0e02DD81DC0C6
ABI — the current controller uses a SINGLE RegistrationArgs tuple, NOT the
legacy 8-positional-arg signature. If you use an old ABI, viem cannot decode
reverts and every call fails silently. Include this exact ABI subset:
struct RegistrationArgs {
string label; // "alice" — do NOT include .eth
address owner;
uint256 duration; // seconds; MIN 28 days (28*24*3600)
bytes32 secret;
address resolver; // PublicResolver above
bytes[] data; // multicall bytes for setAddr etc.
bool reverseRecord;
uint16 ownerControlledFuses; // 0
}
function rentPrice(string label, uint256 duration) view returns (uint256 base, uint256 premium);
function commitment(RegistrationArgs) view returns (bytes32);
function commit(bytes32 commitment);
function register(RegistrationArgs) payable;
// Custom errors — MUST be in your ABI or decoding a revert returns garbage:
error CommitmentTooNew();
error CommitmentTooOld();
error CommitmentDoesNotExist();
error NameNotAvailable(string label);
error DurationTooShort(uint256 duration);
error ResolverRequiredWhenDataSupplied();
error InsufficientValue();
error Unauthorised(bytes32 node);
error MaxCommitmentAgeTooLow();
error MaxCommitmentAgeTooHigh();
Pre-flight guards (render the "Register" button DISABLED until all pass):
1. `BaseRegistrar.controllers(controller)` returns `true`. If false, the
controller isn't DAO-approved — refuse to register and show a copy-paste
"use the canonical controller above" panel. You CANNOT self-approve a
custom controller for a hackathon.
2. Provider is on chainId 11155111 (see Privy TRAP 2).
3. Label matches `/^[a-z0-9-]{3,}$/`. Warn: names <= 4 chars carry a
premium (see rentPrice).
4. `simulateContract(register, args)` succeeds — surfaces
Unauthorised / NameNotAvailable / InsufficientValue BEFORE burning gas
AND before the 60s wait.
Flow (viem + Privy sponsored tx):
1. Compute label, owner (embedded EOA), duration = 365*24*3600.
2. Read [base, premium] = rentPrice(label, duration).
value = (base + premium) * 105n / 100n // 5% buffer for price drift.
3. secret = 32 random bytes. Persist in sessionStorage keyed by
`commit:${label}`; a page refresh mid-wait loses the reveal.
4. args = tuple above with resolver = PublicResolver, data = [
encodeFunctionData(publicResolverAbi, 'setAddr',
[namehash(`${label}.eth`), owner])
], reverseRecord = false, ownerControlledFuses = 0.
5. commitmentHash = commitment(args); send commit(commitmentHash) via
Privy sponsored tx.
6. Wait 65s (chain time drift). If register still reverts with
CommitmentTooNew, poll block.timestamp until
`block.timestamp - controller.commitments(hash) >= minCommitmentAge()`
and auto-retry once.
7. Send register(args) with value. Refunds go back to the EOA.
8. Verify: viem.getEnsAddress({ name: `${label}.eth` }) === owner.
9. Show Etherscan links for commit + register, plus the resolved address.
"app.ens.dev" is NOT proof of ownership — always verify with
`ENSRegistry.owner(namehash(`${label}.eth`))` before showing "registered".
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.
- viem ^2 for reads/writes. `@ensdomains/ensjs` optional for name utilities.
- src/lib/ens.ts: ABIs above + `getSepoliaController()` + `simulate()` helpers.
- src/lib/privy-tx.ts: `sendTxFromEoa(embedded, tx)` implementing the four
Privy traps.
- No Hardhat, no custom contract. Uses the canonical controller directly.
USER FLOW
1. Land -> "Sign in with Google" (Privy) -> embedded EOA auto-provisioned.
2. Enter a label; the UI shows live price + all pre-flight checks green/red.
3. Click "Register" -> commit -> 65s countdown -> register -> success card
with Etherscan link, resolved address, and a "your ENS name" pill used
throughout the rest of the app for the mixed media textile records workflow.
4. 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.
Indicative figures for hackathon pitches — refine with your own research before raising.