FabricTrace Ledger
Track and verify organic fabric sources to assure sustainable fashion claims.
BOB Sepolia smart contract· onchain logic
Section · Onchain
full primer →The primitive.
The onchain primitive runs at the right moment in the flow and surfaces a clear, verifiable result that fashion designers can act on without web3 jargon.
Why this primitiveBOB Sepolia smart contracts ensure immutable, transparent provenance records on-chain.
Kernel
a Solidity contract deployed to BOB Sepolia via MetaMask private key, then verified on BOB Sepolia Explorer
Drives the UI as
a 'verified onchain' badge with the live contract address and a BOB Sepolia Explorer link
Required keys.
ROOTSTOCK_TESTNET_RPC_URL
Alchemy BOB Mainnet HTTPS endpoint. Create a free app → copy the HTTPS URL.
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 "FabricTrace Ledger" in ONE Lovable message. Single-page demo. This prompt is self-contained: everything you need is below — do not ask follow-up questions.
CONCEPT
Track and verify organic fabric sources to assure sustainable fashion claims.
Discipline: Fashion & Textile Design (material provenance).
Onchain primitive: BOB Sepolia smart contract. Why this primitive: BOB Sepolia smart contracts ensure immutable, transparent provenance records on-chain.
5-CREDIT BUDGET (HARD LIMIT):
- ONE single-page app. No router, no Lovable Cloud, no database, no auth flows beyond Privy drop-in.
- ONE Solidity contract, <=80 lines, deployed to BOB Sepolia, verified on BOB Sepolia Explorer (Blockscout).
- Privy is always the auth + sponsored-tx layer (Google login, embedded wallet).
- Pinata/IPFS only if the idea genuinely needs to store a file or metadata.
- At most ONE AI call per user action (use Lovable AI Gateway with LOVABLE_API_KEY if AI is part of the idea).
- Skip tests, skip CI, skip docs pages. Ship the demo, nothing else.
NETWORK — BOB Sepolia (testnet, everything below is required verbatim)
- chainId 808813 (hex 0xc57cd). BOB is an OP-Stack L2 secured by Bitcoin.
- Gas token is ETH. Never BTC, and never the BOB ERC-20 token — holding BOB
tokens on Ethereum L1 does not pay for gas on the BOB network.
- RPC: https://bob-sepolia.rpc.gobob.xyz
- Explorer: https://bob-sepolia.explorer.gobob.xyz (Blockscout, not Etherscan)
- Funding the deployer: claim Sepolia ETH at https://sepolia-faucet.pk910.de/
then bridge it to BOB Sepolia at https://bob-sepolia.gobob.xyz/
- Reference: https://docs.gobob.xyz/docs/user-hub/networks
- Create src/lib/chains.ts exactly like this:
```ts
import type { Chain } from "viem";
export const bobSepolia: Chain = {
id: 808813,
name: "BOB Sepolia",
nativeCurrency: { name: "Sepolia Ether", symbol: "ETH", decimals: 18 },
rpcUrls: {
default: { http: ["https://bob-sepolia.rpc.gobob.xyz"] },
public: { http: ["https://bob-sepolia.rpc.gobob.xyz"] },
},
blockExplorers: {
default: { name: "BOB Sepolia Explorer", url: "https://bob-sepolia.explorer.gobob.xyz" },
},
testnet: true,
};
export const EXPLORER = "https://bob-sepolia.explorer.gobob.xyz";
```
FILE LAYOUT (create every one of these — nothing else)
```text
contracts/FabricTraceLedger.sol the contract source below
hardhat.config.cjs Blockscout verify config below
scripts/deploy.cjs deploy script below
src/lib/chains.ts bobSepolia viem Chain (above)
src/lib/contract.ts ABI + address helper, viem public client
src/data/contract.json { "address": "0x…", "deployTx": "0x…" } written after deploy
src/components/privy-root.tsx ClientOnly + Suspense + lazy wrapper
src/components/privy-client-entry.tsx the ONLY file importing @privy-io/react-auth
src/pages/Index.tsx the entire demo UI (single page)
```
STACK
- React + Vite single page (the index route).
- SSR-safe Privy mount is mandatory. Never import @privy-io/react-auth at
module scope of a route file — it crashes SSR. Use
lazy(() => import('./privy-client-entry')) inside <ClientOnly> + <Suspense>,
and put <PrivyProvider> only inside privy-client-entry.tsx.
```tsx
// src/components/privy-root.tsx
import { lazy, Suspense, type ReactNode } from "react";
import { ClientOnly } from "@tanstack/react-router"; // or a { mounted } useEffect guard on plain Vite
const PrivyClientEntry = lazy(() => import("./privy-client-entry"));
export function PrivyRoot({ children }: { children: ReactNode }) {
return (
<ClientOnly fallback={<div className="p-6 text-sm opacity-60">Loading wallet…</div>}>
<Suspense fallback={<div className="p-6 text-sm opacity-60">Loading wallet…</div>}>
<PrivyClientEntry>{children}</PrivyClientEntry>
</Suspense>
</ClientOnly>
);
}
```
- PrivyProvider config — pass bobSepolia as `defaultChain` + `supportedChains`.
chainId is also passed per-call:
```tsx
// src/components/privy-client-entry.tsx
import { PrivyProvider } from "@privy-io/react-auth";
import { bobSepolia } from "@/lib/chains";
export default function PrivyClientEntry({ children }: { children: React.ReactNode }) {
return (
<PrivyProvider
appId={import.meta.env.VITE_PRIVY_APP_ID}
config={{
loginMethods: ["google", "email"],
embeddedWallets: { ethereum: { createOnLogin: "users-without-wallets" } },
defaultChain: bobSepolia,
supportedChains: [bobSepolia],
appearance: { theme: "dark" },
}}
>
{children}
</PrivyProvider>
);
}
```
- Read the embedded wallet from useWallets, not user.wallet:
const embedded = wallets.find(w => w.walletClientType === 'privy');
- Every send goes through Privy `useSendTransaction` with BOTH `address`
and `sponsor`, wrapped in a 45s Promise.race timeout whose reject message
names the exact dashboard toggles:
await Promise.race([
sendTransaction(
{ to, data, chainId: 808813 },
{ address: embedded.address, sponsor: true }
),
new Promise((_, r) => setTimeout(() => r(new Error(
"Privy sendTransaction timed out after 45s. Enable Gas sponsorship -> App pays -> BOB Sepolia -> Allow transactions from the client."
)), 45_000)),
]);
- Do NOT pass uiOptions:{ showWalletUIs:false } — it aborts with
"signal is aborted without reason". The approval sheet still shows on
the embedded-EOA path; the fee reads US$0.00.
- Do NOT add ZeroDev / SmartWalletsProvider / a paymaster URL. Native
Privy sponsorship on BOB Sepolia works with the toggles above and nothing else.
- DASHBOARD PREREQUISITE (one-time): Privy dashboard -> Gas sponsorship
-> App pays -> add custom chain "BOB Sepolia" (chainId 808813, rpc
https://bob-sepolia.rpc.gobob.xyz) -> toggle "Allow transactions from
the client" ON. Without this, sendTransaction hangs silently.
CONTRACT (contracts/FabricTraceLedger.sol):
```solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
/// @title FabricTraceLedger
/// @notice Track and verify organic fabric sources to assure sustainable fashion claims.
/// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14
contract FabricTraceLedger {
event Logged(address indexed author, string cid, uint256 at);
/// @notice Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14
function log(string calldata cid) external {
emit Logged(msg.sender, cid, block.timestamp);
}
}
```
DEPLOY + VERIFY
- Hardhat lives at the repo root with sources in /contracts (kept outside the
Vite bundle). Install `@nomicfoundation/hardhat-toolbox` AND
`@nomicfoundation/hardhat-verify@latest`.
- hardhat.config.cjs uses hardhat-verify's Blockscout customChain shape
(BOB Sepolia Explorer is Blockscout-based, not Etherscan):
```js
require("@nomicfoundation/hardhat-toolbox");
require("@nomicfoundation/hardhat-verify");
module.exports = {
solidity: { version: "0.8.24", settings: { optimizer: { enabled: true, runs: 200 } } },
networks: {
bobSepolia: {
url: process.env.BOB_SEPOLIA_RPC_URL || "https://bob-sepolia.rpc.gobob.xyz",
accounts: [process.env.METAMASK_PRIVATE_KEY.startsWith("0x")
? process.env.METAMASK_PRIVATE_KEY : "0x" + process.env.METAMASK_PRIVATE_KEY],
chainId: 808813,
},
},
etherscan: {
apiKey: { bobSepolia: "empty" }, // Blockscout ignores the key but the field is required
customChains: [{
network: "bobSepolia",
chainId: 808813,
urls: {
apiURL: "https://bob-sepolia.explorer.gobob.xyz/api",
browserURL: "https://bob-sepolia.explorer.gobob.xyz/",
},
}],
},
sourcify: { enabled: false },
};
```
```js
// scripts/deploy.cjs
const hre = require("hardhat");
const fs = require("fs");
async function main() {
const f = await hre.ethers.getContractFactory("FabricTraceLedger");
const c = await f.deploy();
await c.waitForDeployment();
const address = await c.getAddress();
console.log("deployed:", address);
fs.writeFileSync("src/data/contract.json", JSON.stringify({
address,
deployTx: c.deploymentTransaction().hash,
chainId: 808813,
explorer: "https://bob-sepolia.explorer.gobob.xyz",
}, null, 2));
}
main().catch((e) => { console.error(e); process.exit(1); });
```
- Deploy: `npx hardhat run scripts/deploy.cjs --network bobSepolia`.
- Verify (run RIGHT AFTER deploy, no constructor args for these contracts):
`npx hardhat verify --network bobSepolia <address>`.
On success Blockscout returns "Successfully verified contract" and the
source becomes readable at
`https://bob-sepolia.explorer.gobob.xyz/address/<address>#code`.
Blockscout does NOT auto-verify — always run the command explicitly.
- Frontend reads: create a viem public client with the BOB Sepolia RPC —
`createPublicClient({ chain: bobSepolia, transport: http(import.meta.env.VITE_BOB_SEPOLIA_RPC_URL ?? "https://bob-sepolia.rpc.gobob.xyz") })`.
FRONTEND CODE (src/pages/Index.tsx — adapt copy, keep the choreography)
```tsx
import { useState } from "react";
import { usePrivy, useWallets, useSendTransaction } from "@privy-io/react-auth";
import { createPublicClient, encodeFunctionData, http } from "viem";
import { bobSepolia, EXPLORER } from "@/lib/chains";
import { ABI } from "@/lib/contract";
import contract from "@/data/contract.json";
const client = createPublicClient({ chain: bobSepolia, transport: http() });
export default function Index() {
const { ready, authenticated, login, logout } = usePrivy();
const { wallets } = useWallets();
const { sendTransaction } = useSendTransaction();
const embedded = wallets.find((w) => w.walletClientType === "privy");
const [input, setInput] = useState("");
const [status, setStatus] = useState<"idle" | "pending" | "done" | "error">("idle");
const [txHash, setTxHash] = useState<string | null>(null);
const [error, setError] = useState<string | null>(null);
async function submit() {
if (!embedded) return;
setStatus("pending"); setError(null); setTxHash(null);
try {
const data = encodeFunctionData({ abi: ABI, functionName: "log", args: [input] });
const res: any = await Promise.race([
sendTransaction(
{ to: contract.address as `0x${string}`, data, chainId: 808813 },
{ address: embedded.address, sponsor: true }
),
new Promise((_, r) => setTimeout(() => r(new Error(
"Privy sendTransaction timed out after 45s. Enable Gas sponsorship -> App pays -> BOB Sepolia -> Allow transactions from the client."
)), 45_000)),
]);
const hash = res?.hash ?? res;
setTxHash(hash);
await client.waitForTransactionReceipt({ hash });
setStatus("done");
} catch (e) {
setError(e instanceof Error ? e.message : String(e));
setStatus("error");
}
}
if (!ready) return <main className="p-10">Loading…</main>;
return (
<main className="min-h-screen mx-auto max-w-2xl px-6 py-16">
<h1 className="text-4xl font-semibold tracking-tight">FabricTrace Ledger</h1>
<p className="mt-3 opacity-70">Track and verify organic fabric sources to assure sustainable fashion claims.</p>
{!authenticated ? (
<button onClick={login} className="mt-8 px-6 py-3 rounded-md bg-primary text-primary-foreground">
Sign in with Google
</button>
) : (
<section className="mt-8 space-y-4">
<div className="text-xs font-mono opacity-60">{embedded?.address}</div>
<input value={input} onChange={(e) => setInput(e.target.value)}
placeholder="Describe the material provenance entry…"
className="w-full rounded-md border px-4 py-3 bg-transparent" />
<button onClick={submit} disabled={status === "pending" || !input}
className="px-6 py-3 rounded-md bg-primary text-primary-foreground disabled:opacity-40">
{status === "pending" ? "Writing to BOB Sepolia…" : "Record onchain"}
</button>
{txHash && (
<a href={`${EXPLORER}/tx/${txHash}`} target="_blank" rel="noreferrer"
className="block text-sm underline break-all">
{status === "done" ? "Confirmed" : "Pending"} · {txHash}
</a>
)}
{error && <p className="text-sm text-red-500 break-all">{error}</p>}
<button onClick={logout} className="text-xs uppercase tracking-widest opacity-60">Sign out</button>
</section>
)}
<footer className="mt-20 text-xs opacity-60">Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14</footer>
</main>
);
}
```
Also export the ABI from src/lib/contract.ts so the page and any read call share it.
HOOK INTEGRATION — direct contract write
- One write function, one read. After the tx confirms, re-read the contract
(or the emitted `Logged` event) and render the result from chain state — not
from local component state — so a refresh shows the same thing.
```ts
const logs = await client.getContractEvents({
address: contract.address as `0x${string}`,
abi: ABI,
eventName: "Logged",
fromBlock: "earliest",
});
```
- Show a running feed of the last few entries with author, value and timestamp,
each linking to `${EXPLORER}/tx/<hash>`.
USER FLOW
1. Land on page -> 'Sign in with Google' (Privy) -> embedded wallet auto-provisioned.
2. User performs a material provenance action; the app calls `log(payload)` on the contract via Privy sponsored tx and shows the BOB Sepolia Explorer link as proof.
3. While the tx is in flight the button reads 'Writing to BOB Sepolia…' and the hash appears as a pending link; on receipt it flips to 'Confirmed'.
4. The page re-reads chain state so a refresh shows the same result.
5. Footer renders: "Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14"
DESIGN DIRECTION
Commit to one distinctive look drawn from fashion & textile design — specifically
material provenance. No purple-on-white gradient hero, no Inter/Poppins default stack, no
generic three-card feature row. Pick a real typeface pairing, one accent colour
plus a near-monochrome base, and a layout with an obvious focal point. Define
colours as CSS variables/tokens; never hardcode text-white or bg-black. Dark by
default. The proof-of-onchain elements (address, tx hash, CID) should read as
deliberate typographic detail in a mono face, not as debug output.
TROUBLESHOOTING (these are the failures you will actually hit)
- Blank page / "document is not defined" on load -> @privy-io/react-auth was
imported at module scope of a route. Move it behind the lazy client entry.
- sendTransaction never resolves, no error -> the Privy dashboard gas-sponsorship
toggle for BOB Sepolia is off. Add the custom chain and enable "Allow
transactions from the client".
- "signal is aborted without reason" -> you passed uiOptions:{ showWalletUIs:false }.
Remove it.
- Tx reverts with EMPTY revert data -> you encoded a function the deployed
contract does not have. Check the verified ABI on Blockscout before
debugging the UI.
- Tx sits pending forever -> underpriced. Resend the same nonce with a higher
maxFeePerGas (~5 gwei) and 1 gwei priority fee.
- `hardhat verify` fails with "missing API key" -> keep the dummy
`apiKey: { bobSepolia: "empty" }` entry; Blockscout ignores the value but
hardhat-verify requires the field.
- Explorer shows "not verified" after deploy -> Blockscout never auto-verifies.
Run the verify command.
- Reads return nothing -> you are pointed at the wrong chain. Confirm chainId
808813 and the BOB Sepolia RPC, not Ethereum Sepolia (11155111).
REQUIRED SECRETS (Lovable -> Project Settings -> Secrets):
- METAMASK_PRIVATE_KEY BOB Sepolia deployer key. Fund flow: get Sepolia ETH from https://sepolia-faucet.pk910.de/ then bridge to BOB Sepolia at https://bob-sepolia.gobob.xyz/
- BOB_SEPOLIA_RPC_URL HTTPS RPC (default: https://bob-sepolia.rpc.gobob.xyz). Public RPC works for hackathon load; swap for a dedicated node provider if you need higher rate limits.
- PRIVY_APP_ID Google sign-in + sponsored tx. Docs: https://docs.privy.io/llms-full.txt
- PINATA_JWT IPFS uploads (only if app pins media). Docs: https://docs.pinata.cloud/llms-full.txt
CREDIT (must appear in UI footer AND as NatSpec on every deployed contract):
Built during the Creative AI & Quantum Hackathon organised by StreetKode Fam during Indian Krump Festival 14
Market sizing.
TAM
$1.2B
fashion design software market
SAM
$250M
sustainable textile tracking tools
SOM
$15M
organic fabric brands using blockchain verification
Indicative figures for hackathon pitches — refine with your own research before raising.
Adjacent entries.
design copyright
DesignAuth Mint
Securely register and timestamp original fashion designs to prevent copying.
color curationColorPalette DAO
Collaboratively create and share color palettes with verified ownership and royalty rules.
virtual try-onVirtualFitting NFT
Create unique virtual fitting sessions minted as NFTs for secure client access.
recycled textile verificationUpcycleProof Chain
Authenticate recycled textiles’ origin to validate upcycled fashion products.